diff --git a/README.md b/README.md
index f0b53f9..bea1f88 100644
--- a/README.md
+++ b/README.md
@@ -1,29 +1,40 @@
-# Python библиотека для всех [API Яндекс Метрика](https://yandex.ru/dev/metrika/doc/api2/concept/about-docpage/)
+# Python client for all [API Yandex Metrika](https://yandex.ru/dev/metrika/doc/api2/concept/about-docpage/)
-Написано на версии python 3.5
+![Supported Python Versions](https://img.shields.io/static/v1?label=python&message=>=3.5&color=red)
+[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/vintasoftware/tapioca-wrapper/master/LICENSE)
+[![Downloads](https://pepy.tech/badge/tapi-yandex-metrika)](https://pepy.tech/project/tapi-yandex-metrika)
+
-## Установка
-```
-pip install tapi-yandex-metrika
-```
+## Installation
-Документация по API [управления счетчиком](https://github.com/pavelmaksimov/tapi-yandex-metrika/blob/master/docs/management.md)
+Prev version
-Документация по API [получения отчетов](https://github.com/pavelmaksimov/tapi-yandex-metrika/blob/master/docs/stats.md)
+ pip install --upgrade tapi-yandex-metrika==2020.10.20
-Документация по [Logs API](https://github.com/pavelmaksimov/tapi-yandex-metrika/blob/master/docs/logsapi.md)
+Last version. Has backward incompatible changes.
-## Зависимости
-- requests
-- [tapi_wrapper](https://github.com/pavelmaksimov/tapi-wrapper)
+ pip install --upgrade tapi-yandex-metrika==2021.2.21
-## Автор
-Павел Максимов
+## Documentation
-Связаться со мной можно в
-[Телеграм](https://t.me/pavel_maksimow)
-и в
+[API Management](https://github.com/pavelmaksimov/tapi-yandex-metrika/blob/master/docs/management.md)
+
+[API Stats](https://github.com/pavelmaksimov/tapi-yandex-metrika/blob/master/docs/stats.md)
+
+[Logs API](https://github.com/pavelmaksimov/tapi-yandex-metrika/blob/master/docs/logsapi.md)
+
+## Dependencies
+- requests
+- [tapi_wrapper](https://github.com/pavelmaksimov/tapi-wrapper)
+
+## Authors
+Pavel Maksimov
+
+[Telegram](https://t.me/pavel_maksimow),
[Facebook](https://www.facebook.com/pavel.maksimow)
+Good luck friend! Put an asterisk;)
+
Удачи тебе, друг! Поставь звездочку ;)
+Copyright (c) Pavel Maksimov.
diff --git a/docs/logsapi.md b/docs/logsapi.md
index 48a7554..5e6d425 100644
--- a/docs/logsapi.md
+++ b/docs/logsapi.md
@@ -1,75 +1,91 @@
-## Документация по Logs API
+# Documentation for downloading reports from Yandex Metrika LOGS API
+
+[Official documentation Yandex Metrika LOGS API](https://yandex.ru/dev/metrika/doc/api2/api_v1/data.html)
+
```python
from tapi_yandex_metrika import YandexMetrikaLogsapi
+ACCESS_TOKEN =
+COUNTER_ID =
-ACCESS_TOKEN = {ваш токен доступа}
-COUNTER_ID = {идентификатор счетчика}
-
-api = YandexMetrikaLogsapi(
+client = YandexMetrikaLogsapi(
access_token=ACCESS_TOKEN,
default_url_params={'counterId': COUNTER_ID}
)
-params={
+params = {
"fields": "ym:s:date,ym:s:clientID",
"source": "visits",
- "date1": "2019-01-01",
- "date2": "2019-01-01"
+ "date1": "2021-01-01",
+ "date2": "2021-01-01"
}
-# Проверить возможность создания отчета. Через HTTP метод GET.
-result = api.evaluate().get(params=params)
+# Check the possibility of creating a report. Via HTTP GET method.
+result = client.evaluate().get(params=params)
print(result)
-# Заказать отчет. Через HTTP метод POST.
-result = api.create().post(params=params)
-request_id = result().data["log_request"]["request_id"]
+# Order a report. Via HTTP POST method.
+result = client.create().post(params=params)
+request_id = result["log_request"]["request_id"]
print(result)
-# Отменить создание отчета. Через HTTP метод POST.
-result = api.cancel(requestId=request_id).post()
+# Cancel report creation. Via HTTP POST method.
+result = client.cancel(requestId=request_id).post()
print(result)
-# Удалить отчет. Через HTTP метод POST.
-result = api.clean(requestId=request_id).post()
+# Delete report. Via HTTP POST method.
+result = client.clean(requestId=request_id).post()
print(result)
-# Получить информацию обо всех отчетах хранящихся на сервере. Через HTTP метод GET.
-result = api.allinfo().get()
+# Get information about all reports stored on the server. Via HTTP GET method.
+result = client.allinfo().get()
print(result)
-# Получить информацию о конкретном отчете. Через HTTP метод GET.
-result = api.info(requestId=request_id).get()
+# Get information about a specific report. Via HTTP GET method.
+result = client.info(requestId=request_id).get()
print(result)
-# Скачать отчет. Через HTTP метод POST.
-result = api.create().post(params=params)
-request_id = result().data["log_request"]["request_id"]
-
-# Отчет можно скачать, когда он будет сформирован на сервере. Через HTTP метод GET.
-result = api.info(requestId=request_id).get()
-if result["log_request"]["status"] == "processed":
- # Отчет может состоять из нескольких частей.
- parts = result["log_request"]["parts"] # Кол-во частей в отчете.
- print("Кол-во частей", parts)
- # В параметр partNumber указывается номер части отчета, который хотите скачать.
- # Скачаем первую часть.
- result = api.download(requestId=request_id, partNumber=0).get()
- data = result().data
- print(data[:1000])
+# Download the report. Via HTTP POST method.
+result = client.create().post(params=params)
+request_id = result["log_request"]["request_id"]
+
+# The report can be downloaded when it is generated on the server. Via HTTP GET method.
+info = client.info(requestId=request_id).get()
+if info["log_request"]["status"] == "processed":
+
+ # The report can consist of several parts.
+ parts = info["log_request"]["parts"]
+ print("Number of parts in the report", parts)
+
+ # The partNumber parameter specifies the number of the part of the report that you want to download.
+ # Default partNumber=0
+ part = client.download(requestId=request_id, partNumber=0).get()
+
+ print("Raw data")
+ data = part.data[:1000]
+
+ print("Column names")
+ print(part.columns)
+
+ # Transform to values
+ print(part().to_values()[:3])
+
+ # Transform to lines
+ print(part().to_lines()[:3])
else:
- print("Отчет еще не готов")
+ print("Report not ready yet")
```
-В библиотеке есть функция, которая
-подождет, когда отчет будет сформирован и скачает все его части.
+#### Download the report when it will be created
+
+add param **wait_report**
+
```python
from tapi_yandex_metrika import YandexMetrikaLogsapi
@@ -79,12 +95,8 @@ COUNTER_ID = {идентификатор счетчика}
api = YandexMetrikaLogsapi(
access_token=ACCESS_TOKEN,
default_url_params={'counterId': COUNTER_ID},
- # Если True, скачает первую часть отчета, когда он будет сформирован.
- # По умолчанию False.
+ # Download the report when it will be created
wait_report=True,
- # Если True, будет скачивать все части отчета.
- # По умолчанию False.
- receive_all_data=True
)
params={
"fields": "ym:s:date,ym:s:clientID,ym:s:dateTime,ym:s:startURL,ym:s:endURL",
@@ -92,64 +104,121 @@ params={
"date1": "2019-01-01",
"date2": "2019-01-01"
}
-result = api.create().post(params=params)
-request_id = result().data["log_request"]["request_id"]
-# Когда включен параметр receive_all_data=True, параметр partNumber можно не указывать.
-result = api.download(requestId=request_id).get()
-data = result().data
-print(data[:1000])
-```
+info = client.create().post(params=params)
+request_id = info["log_request"]["request_id"]
-Есть метод преобразования данных для ресурса **download**.
-```python
-result = api.download(requestId=request_id).get()
-data_as_json = result().transform()
-print(data_as_json[:2])
-[
- ['ym:s:date', 'ym:s:startOfMonth', 'ym:s:visits', 'ym:s:bounces'],
- ['2019-09-26', '2019-09-01', 80384.0, 9389.0]
- ]
-```
+report = client.download(requestId=request_id).get()
-Можно получить информацию о последнем сделанном запросе
+print("Column names")
+print(report.columns)
-```python
-result = api.allinfo().get()
-print(result().status_code)
-print(result().response)
-print(result().response.headers)
+print("Raw data")
+data = report.data[:1000]
+
+# Transform to values
+print(report().to_values()[:3])
+
+# Transform to lines
+print(report().to_lines()[:3])
```
-Обращайте внимание каким HTTP методом вы отправляете запрос.
-Некоторые ресурсы работают только с POST или только с GET запросами.
-Например ресурс **create** только с методом POST
+#### Export of all report pages.
+```python
+# Iteration parts.
+report = client.download(requestId=request_id).get()
+print(report.columns)
+for part in report().parts():
+ print(part.data[:1000])
+ print(part().to_values()[:3])
+ print(part().to_lines()[:3])
+ print(part().to_columns()) # columns data orient
+
+for part in report().parts():
+ # Iteration lines.
+ for line in part().lines():
+ print('line', line)
- api.create().post(params=params)
+ # Iteration values.
+ for values in part().values():
+ print('values', values)
-А метод **evaluate** только с методом GET
- api.evaluate().get(params=params)
+# "Will iterate over all lines of all parts"
+report = client.download(requestId=request_id).get()
+print(report.columns)
-## Фичи
+for line in report().iter_lines():
+ print('line', line)
-Можно вывести на печать описание ресурса через метод info
-```python
-# Указываете интересующий ресурс, а после него .info()
-api.create().info()
+for values in report().iter_values():
+ print('values', values)
```
-Открыть документацию ресурса в браузере
+#### Limit iteration
+
+ .parts(max_parts: int = None)
+ .lines(max_items: int = None)
+ .values(max_items: int = None)
+ .iter_lines(max_parts: int = None, max_items: int = None)
+ .iter_values(max_parts: int = None, max_items: int = None)
+
+#### Resonse
+
```python
-api.create().open_docs()
+info = client.allinfo().get()
+print(info.response)
+print(info.response.headers)
+print(info.response.status_code)
+
+report = client.download(requestId=request_id).get()
+for part in report().parts():
+ print(part.response)
+ print(part.response.headers)
+ print(part.response.status_code)
```
-## Автор
-Павел Максимов
+#### Warning
+Pay attention to which HTTP method you send the request.
+Some resources work only with POST or only with GET requests.
+For example create resource with POST method only
+
+ client.create().post(params=params)
+
+And evaluate method only with GET method
-Связаться со мной можно в
-[Телеграм](https://t.me/pavel_maksimow)
-и в
+ client.evaluate().get(params=params)
+
+
+## Authors
+Pavel Maksimov -
+[Telegram](https://t.me/pavel_maksimow),
[Facebook](https://www.facebook.com/pavel.maksimow)
+Good luck friend! Put an asterisk;)
+
Удачи тебе, друг! Поставь звездочку ;)
+
+Copyright (c) Pavel Maksimov.
+
+## Change log
+### Release 2021.2.21
+
+**Backward Incompatible Change**
+
+- Drop method "transform"
+- Drop param "receive_all_data"
+
+**New Feature**
+- translated into english
+- add iteration method "parts"
+- add iteration method "lines"
+- add iteration method "values"
+- add iteration method "iter_lines"
+- add iteration method "iter_values"
+- add attribut "columns"
+- add attribut "data"
+- add attribut "response"
+- add method "to_lines"
+- add method "to_values"
+- add method "to_columns"
diff --git a/docs/management.md b/docs/management.md
index f1f760e..40955ba 100644
--- a/docs/management.md
+++ b/docs/management.md
@@ -1,4 +1,7 @@
-## Документация по API управления счетчиком Я.Метрики
+# Yandex Metrika Counter Management API Documentation
+
+[Official documentation API Yandex Metrika](https://yandex.ru/dev/metrika/doc/api2/management/intro.html)
+
``` python
from tapi_yandex_metrika import YandexMetrikaManagement
@@ -6,26 +9,66 @@ from tapi_yandex_metrika import YandexMetrikaManagement
ACCESS_TOKEN = {ваш токен доступа}
COUNTER_ID = {идентификатор счетчика}
-api = YandexMetrikaManagement(
+client = YandexMetrikaManagement(
access_token=ACCESS_TOKEN,
default_url_params={'counterId': COUNTER_ID}
)
```
-Генерация класса YandexMetrikaManagement происходит динамически, поэтому узнать о добавленных ресурсах API, можно так.
-
- print(dir(api))
+### Method help
+```python
+# The YandexMetrikaManagement class is created dynamically,
+# so you can find out the available methods after initialization.
+print(dir(client))
+['accounts', 'chart_annotation', 'chart_annotations', 'clients', 'counter', 'counter_undelete',
+ 'counters', 'delegate', 'delegates', 'filter', 'filters', 'goal', 'goals', 'grant', 'grants', 'label',
+ 'labels', 'offline_conversions_calls_extended_threshold', 'offline_conversions_calls_uploading',
+ 'offline_conversions_calls_uploadings', 'offline_conversions_extended_threshold',
+ 'offline_conversions_upload', 'offline_conversions_upload_calls', 'offline_conversions_uploading',
+ 'offline_conversions_uploadings', 'operation', 'operations', 'public_grant', 'segment', 'segments',
+ 'set_counter_label', 'user_params_upload', 'user_params_uploading', 'user_params_uploading_confirm',
+ 'user_params_uploadings', 'yclid_conversions_upload', 'yclid_conversions_uploading',
+ 'yclid_conversions_uploadings']
+
+# Help information about the method
+client.counters().help()
+# Documentation: https://yandex.ru/dev/metrika/doc/api2/management/direct_clients/getclients-docpage/
+# Resource path: management/v1/clients
+# Available HTTP methods:
+# ['GET']
+# Available query parameters:
+# 'counters='
+
+# Open resource documentation in a browser
+client.counters().open_docs()
+```
-Пример
+How to send different types of HTTP requests
+```python
+# GET request
+client.counters().get(params={})
+# POST request
+client.counters().post(data={}, params={})
+# DELETE request
+client.counters().delete(...)
+# PUT request
+client.counters().put(...)
+# PATCH request
+client.counters().patch(...)
+# OPTIONS request
+client.counters().options(...)
+```
```python
-# Получить счетчики. Через HTTP метод GET.
-api.counters().get()
+# Get counters. Via HTTP GET method.
+counters = client.counters().get()
+print(counters.data)
-# Получить счетчики с сортировкой по визитам. Через HTTP метод GET.
-api.counters().get(params={"sort": "Visits"})
+# Get counters sorted by visit. Via HTTP GET method.
+counters = client.counters().get(params={"sort": "Visits"})
+print(counters.data)
-# Создать цель. Через HTTP метод POST.
+# Create a goal. Via HTTP POST method.
body = {
"goal": {
"name": "2 страницы",
@@ -34,9 +77,9 @@ body = {
"depth": 2
}
}
-api.goals().post(data=body)
+client.goals().post(data=body)
-# Создать цель по событию JavaScript. Через HTTP метод POST.
+# Create target on JavaScript event. Via HTTP POST method.
body2 = {
"goal": {
"name": "Название вашей цели в метрике",
@@ -45,20 +88,19 @@ body2 = {
"conditions": [
{
"type": "exact",
- #Значение newlead заменить на свой идентификатор события
- "url": "newlead"
+ "url":
}
]
}
}
-api.goals().post(data=body2)
+client.goals().post(data=body2)
-# Для некоторых ресурсов необходимо подставлять в url идентификатор объекта.
-# Это делается путем добавления в сам метод идентификатора.
-# Получить информацию о цели. Через HTTP метод GET.
-api.goal(goalId=10000).get()
+# For some resources, you need to substitute the object identifier in the url.
+# This is done by adding an identifier to the method itself.
+# Get information about the target. Via HTTP GET method.
+client.goal(goalId=10000).get()
-# Изменить цель. Через HTTP метод PUT.
+# Change target. Via HTTP PUT method.
body = {
"goal" : {
"id" : ,
@@ -68,50 +110,40 @@ body = {
...
}
}
-api.goal(goalId=10000).put(data=body)
+client.goal(goalId=10000).put(data=body)
-# Удалить цель. Через HTTP метод DELETE.
-api.goal(goalId=10000).delete()
+# Delete target. Via HTTP DELETE method.
+client.goal(goalId=10000).delete()
```
-Доступные параметры ресурсов и идентификаторы объектов, которые нужно обязательно указывать в методе, ищите в
-[справке](https://yandex.ru/dev/metrika/doc/api2/management/intro-docpage/)
-и/или в [карте ресурсов](https://github.com/pavelmaksimov/tapi-yandex-metrika/blob/master/tapi_yandex_metrika/resource_mapping.py).
+For available resource parameters and object identifiers that must be specified in the method, look in
+[help](https://yandex.ru/dev/metrika/doc/api2/management/intro-docpage/)
+and or in [resource map](https://github.com/pavelmaksimov/tapi-yandex-metrika/blob/master/tapi_yandex_metrika/resource_mapping.py).
-#### Получить данные ответа.
+You can get information about the request.
```python
-result = api.counters().get()
-data = result().data
-print(data)
+counters = client.counters().get()
+print(counters.response)
+print(counters.response.headers)
+print(counters.response.status_code)
```
-Можно получить информацию о запросе.
-```python
-print(result)
-print(result().status_code)
-print(result().response)
-print(result().response.headers)
-```
-## Фичи
+## Authors
+Pavel Maksimov -
+[Telegram](https://t.me/pavel_maksimow),
+[Facebook](https://www.facebook.com/pavel.maksimow)
-Открыть документацию ресурса в браузере
-```python
-api.counters().open_docs()
-```
+Good luck friend! Put an asterisk;)
-Послать запрос в браузере
-```python
-api.counters().open_in_browser()
-```
+Удачи тебе, друг! Поставь звездочку ;)
-## Автор
-Павел Максимов
+Copyright (c) Pavel Maksimov.
-Связаться со мной можно в
-[Телеграм](https://t.me/pavel_maksimow)
-и в
-[Facebook](https://www.facebook.com/pavel.maksimow)
+## Change log
+### Release 2021.2.21
-Удачи тебе, друг! Поставь звездочку ;)
+**New Feature**
+- add attribut "data"
+- add attribut "response"
diff --git a/docs/stats.md b/docs/stats.md
index 8a3c0f8..f3f45bb 100644
--- a/docs/stats.md
+++ b/docs/stats.md
@@ -1,99 +1,147 @@
-## Документация по скачиванию отчетов из Я.Метрика
+[Official documentation API Yandex Metrika](https://yandex.ru/dev/metrika/doc/api2/api_v1/data.html)
+
+# Documentation for downloading reports from API Yandex Metrika (Как скачать данные из API Яндекс Метрика)
``` python
+import datetime as dt
from tapi_yandex_metrika import YandexMetrikaStats
-ACCESS_TOKEN = {ваш токен доступа}
-COUNTER_ID = {номер счетчика Я.Метрики}
+ACCESS_TOKEN =
+COUNTER_ID =
-api = YandexMetrikaStats(access_token=ACCESS_TOKEN)
+client = YandexMetrikaStats(access_token=ACCESS_TOKEN)
params = dict(
ids=COUNTER_ID,
- metrics="ym:s:visits,ym:s:bounces",
- dimensions="ym:s:date,ym:s:startOfMonth",
+ date1="2020-10-01",
+ date2=dt.date(2020,10,5),
+ metrics="ym:s:visits",
+ dimensions="ym:s:date",
sort="ym:s:date",
limit=5
+ # Other params -> https://yandex.ru/dev/metrika/doc/api2/api_v1/data.html
)
-result = api.stats().get(params=params)
-print(result().data)
-
-# По умолчанию возвращаются только 10000 строк отчета,
-# если не указать другое кол-во в параметре limit.
-# В отчете может быть больше строк, чем указано в limit
-# Тогда необходимо сделать несколько запросов для получения всего отчета.
-# Чтоб сделать это автоматически вы можете указать
-# параметр receive_all_data=True при инициализации класса.
-
-api = YandexMetrikaStats(
- access_token=ACCESS_TOKEN,
- # Если True, будет скачивать все части отчета. По умолчанию False.
- receive_all_data=True
-)
-params = dict(
- ids=COUNTER_ID,
- metrics="ym:s:visits,ym:s:bounces",
- dimensions="ym:s:date,ym:s:startOfMonth",
- sort="ym:s:date",
- limit=10
-)
-result = api.stats().get(params=params)
-print(result().data)
-```
+report = client.stats().get(params=params)
+
+# Response data
+print(report.data)
+
+report.columns
+# ['ym:s:date', 'ym:s:visits']
+
+report().to_values()
+#[
+# ['2020-10-01', 14234.0],
+# ['2020-10-02', 12508.0],
+# ['2020-10-03', 12365.0],
+# ['2020-10-04', 14588.0],
+# ['2020-10-05', 14579.0]
+#]
+
+# Column data orient
+report().to_columns()
+#[
+# ['2020-10-01', '2020-10-02', '2020-10-03', '2020-10-04', '2020-10-05'],
+# [14234.0, 12508.0, 12365.0, 14588.0, 14579.0]
+#]
-В params можно передать [много других параметров](https://yandex.ru/dev/metrika/doc/api2/api_v1/data-docpage/).
-
-#### Получить данные ответа.
-```python
-result = api.stats().get(params=params)
-data = result().data
-print(data)
-[{json_data}, {json_data},] # В списке может находится несколько ответов
-```
-
-##### Преобразование ответа
-
-Для ответов API отчетов есть функция преобразования **transform**.
-Она соединяет все ответы в один список.
-```python
-result = api.stats().get(params=params)
-data = result().transform()
-print(data)
-[['ym:s:date', 'ym:s:startOfMonth', 'ym:s:visits', 'ym:s:bounces'],
- ['2019-09-26', '2019-09-01', 80384.0, 9389.0]]
```
-Можно получить информацию о последнем запросе.
-```python
-print(result().status_code)
-print(result().response)
-print(result().response.headers)
+#### Export of all report pages.
+``` python
+print("Iteratin report pages")
+for page in report().pages():
+ page_values = page().to_values()
+ print(page_values)
+# [['2020-10-01', 14234.0], ['2020-10-02', 12508.0], ['2020-10-03', 12365.0], ['2020-10-04', 14588.0], ['2020-10-05', 14579.0]]
+# [['2020-10-06', 12795.0]]
+
+
+print("Iteratin report rows")
+for page in report().pages():
+ for row in page().rows():
+ print(row)
+# ['2020-10-01', 14234.0]
+# ['2020-10-02', 12508.0]
+# ['2020-10-03', 12365.0]
+# ['2020-10-04', 14588.0]
+# ['2020-10-05', 14579.0]
+# ['2020-10-06', 12795.0]
+
+
+print("Will iterate over all lines of all pages")
+for row in report().iter_rows():
+ print(row)
+# ['2020-10-01', 14234.0]
+# ['2020-10-02', 12508.0]
+# ['2020-10-03', 12365.0]
+# ['2020-10-04', 14588.0]
+# ['2020-10-05', 14579.0]
+# ['2020-10-06', 12795.0]
```
-## Фичи
+#### Iteration limit.
-Вывести описание ресурса
-```python
-# Указываете интересующий ресурс, а после него .info()
-api.stats().info()
-```
+ .pages(max_pages: int = None)
+ .rows(max_items: int = None)
+ .iter_rows(max_pages: int = None, max_items: int = None)
-Открыть документацию ресурса в браузере
-```python
-api.stats().open_docs()
+``` python
+print("Iteratin report rows with limit")
+for page in report().pages(max_pages=2):
+ for row in page().rows(max_items=2):
+ print(row)
+# ['2020-10-01', 14234.0]
+# ['2020-10-02', 12508.0]
+# ['2020-10-06', 12795.0]
+
+
+print("Will iterate over all lines of all pages with limit")
+for row in report().iter_rows(max_pages=2, max_items=1):
+ print(row)
+# ['2020-10-01', 14234.0]
```
-Послать запрос в браузере
+#### Response
```python
-api.stats().open_in_browser()
+report = client.stats().get(params=params)
+print(report.response)
+print(report.response.status_code)
+print(report.response.headers)
+
+for page in report().pages():
+ print(page.response)
+ print(page.response.status_code)
+ print(page.response.headers)
```
-## Автор
-Павел Максимов
-Связаться со мной можно в
-[Телеграм](https://t.me/pavel_maksimow)
-и в
+## Authors
+Pavel Maksimov -
+[Telegram](https://t.me/pavel_maksimow),
[Facebook](https://www.facebook.com/pavel.maksimow)
+Good luck friend! Put an asterisk;)
+
Удачи тебе, друг! Поставь звездочку ;)
+
+Copyright (c) Pavel Maksimov.
+
+## Change log
+### Release 2021.2.21
+
+**Backward Incompatible Change**
+
+- Drop method "transform"
+- Drop param "receive_all_data"
+
+**New Feature**
+- translated into english
+- add iteration method "pages"
+- add iteration method "rows"
+- add iteration method "iter_rows"
+- add attribut "columns"
+- add attribut "data"
+- add attribut "response"
+- add method "to_values"
+- add method "to_columns"
diff --git a/examples/stats.py b/examples/stats.py
new file mode 100644
index 0000000..14d8bac
--- /dev/null
+++ b/examples/stats.py
@@ -0,0 +1,96 @@
+import datetime as dt
+import logging
+import yaml
+
+from tapi_yandex_metrika import YandexMetrikaStats
+
+logging.basicConfig(level=logging.DEBUG)
+
+with open("../config.yml", "r") as stream:
+ data_loaded = yaml.safe_load(stream)
+
+ACCESS_TOKEN = data_loaded["token"]
+COUNTER_ID = data_loaded["counter_id"]
+
+api = YandexMetrikaStats(access_token=ACCESS_TOKEN)
+
+
+params = dict(
+ ids=COUNTER_ID,
+ date1="2020-10-01",
+ date2=dt.date(2020,10,6),
+ metrics="ym:s:visits",
+ dimensions="ym:s:date",
+ sort="ym:s:date",
+ limit=5
+ # Other params -> https://yandex.ru/dev/metrika/doc/api2/api_v1/data.html
+)
+report = api.stats().get(params=params)
+
+# Response data
+print(report.data)
+
+print(report.columns)
+# ['ym:s:date', 'ym:s:visits']
+
+print(report().to_values())
+#[
+# ['2020-10-01', 14234.0],
+# ['2020-10-02', 12508.0],
+# ['2020-10-03', 12365.0],
+# ['2020-10-04', 14588.0],
+# ['2020-10-05', 14579.0]
+#]
+
+
+print(report().to_columns())
+#[
+# ['2020-10-01', '2020-10-02', '2020-10-03', '2020-10-04', '2020-10-05'],
+# [14234.0, 12508.0, 12365.0, 14588.0, 14579.0]
+#]
+
+
+print("Iteratin report pages")
+for page in report().pages():
+ page_values = page().to_values()
+ print(page_values)
+# [['2020-10-01', 14234.0], ['2020-10-02', 12508.0], ['2020-10-03', 12365.0], ['2020-10-04', 14588.0], ['2020-10-05', 14579.0]]
+# [['2020-10-06', 12795.0]]
+
+
+print("Iteratin report rows")
+for page in report().pages():
+ for row in page().rows():
+ print(row)
+# ['2020-10-01', 14234.0]
+# ['2020-10-02', 12508.0]
+# ['2020-10-03', 12365.0]
+# ['2020-10-04', 14588.0]
+# ['2020-10-05', 14579.0]
+# ['2020-10-06', 12795.0]
+
+
+print("Will iterate over all lines of all pages")
+for row in report().iter_rows():
+ print(row)
+# ['2020-10-01', 14234.0]
+# ['2020-10-02', 12508.0]
+# ['2020-10-03', 12365.0]
+# ['2020-10-04', 14588.0]
+# ['2020-10-05', 14579.0]
+# ['2020-10-06', 12795.0]
+
+
+print("Iteratin report rows with limit")
+for page in report().pages(max_pages=2):
+ for row in page().rows(max_items=2):
+ print(row)
+# ['2020-10-01', 14234.0]
+# ['2020-10-02', 12508.0]
+# ['2020-10-06', 12795.0]
+
+
+print("Will iterate over all lines of all pages with limit")
+for row in report().iter_rows(max_pages=2, max_items=1):
+ print(row)
+# ['2020-10-01', 14234.0]
diff --git a/setup.py b/setup.py
index 75ef220..22d4d28 100644
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,4 @@
#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
try:
from setuptools import setup
@@ -29,7 +27,7 @@ def get_version(package):
setup(
name="tapi-yandex-metrika",
version=get_version(package),
- description="Python библиотека для API Яндекс Метрики",
+ description="Python client for API Yandex Metrika",
long_description=readme,
long_description_content_type="text/markdown",
author="Pavel Maksimov",
@@ -37,7 +35,7 @@ def get_version(package):
url="https://github.com/pavelmaksimov/tapi-yandex-metrika",
packages=[package],
include_package_data=True,
- install_requires=["requests-oauthlib>=0.4.2", "simplejson", "tapi-wrapper==2019.12.10"],
+ install_requires=["orjson", "tapi-wrapper2>=0.1,<0.2"],
license="MIT",
zip_safe=False,
keywords="tapi,wrapper,yandex,metrika,api",
diff --git a/tapi_yandex_metrika/__init__.py b/tapi_yandex_metrika/__init__.py
index 67b29f2..f5bbdda 100644
--- a/tapi_yandex_metrika/__init__.py
+++ b/tapi_yandex_metrika/__init__.py
@@ -1,12 +1,11 @@
-# -*- coding: utf-8 -*-
__author__ = 'Pavel Maksimov'
__email__ = 'vur21@ya.com'
-__version__ = '2020.10.20'
+__version__ = '2021.2.21'
+from .resource_mapping import *
from .tapi_yandex_metrika import (
YandexMetrikaStats,
YandexMetrikaLogsapi,
YandexMetrikaManagement
)
-from .resource_mapping import *
diff --git a/tapi_yandex_metrika/exceptions.py b/tapi_yandex_metrika/exceptions.py
index 3cb297e..3337d44 100644
--- a/tapi_yandex_metrika/exceptions.py
+++ b/tapi_yandex_metrika/exceptions.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
import logging
@@ -18,21 +17,14 @@ def __str__(self):
)
-class YandexMetrikaServerError(YandexMetrikaApiError):
- pass
-
-
class YandexMetrikaClientError(YandexMetrikaApiError):
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.jdata = self.response.json()
- self.code = self.jdata.get("code")
- self.message = self.jdata.get("message")
- self.errors = self.jdata.get("errors")
+ def __init__(self, response, message=None, code=None, errors=None):
+ super().__init__(response, message)
+ self.code = code
+ self.message = message
+ self.errors = errors
def __str__(self):
- logging.info("HEADERS = " + str(self.response.headers))
- logging.info("URL = " + self.response.url)
return "code={}, message={}, errors={}".format(
self.code, self.message, self.errors
)
@@ -43,15 +35,27 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
-class YandexMetrikaLimitError(YandexMetrikaApiError):
+class YandexMetrikaLimitError(YandexMetrikaClientError):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+
+class YandexMetrikaDownloadReportError(YandexMetrikaClientError):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
+ def __str__(self):
+ return self.message
+
+
+class BackwardCompatibilityError(Exception):
+ def __init__(self, name):
+ self.name = name
+
def __str__(self):
return (
- "{} {} Исчерпан лимит запросов. "
- "Повторите запрос через некоторое время.\n "
- "{}".format(
- self.response.status_code, self.response.reason, self.response.text
- )
- )
+ "Starting from version 2021.2.21, this {} is deprecated and not supported. "
+ "Install a later version "
+ "'pip install --upgrade tapi-yandex-metrika==2020.10.20'. "
+ "Info https://github.com/pavelmaksimov/tapi-yandex-metrika"
+ ).format(self.name)
diff --git a/tapi_yandex_metrika/resource_mapping.py b/tapi_yandex_metrika/resource_mapping.py
index 3fbbb44..52de9ad 100644
--- a/tapi_yandex_metrika/resource_mapping.py
+++ b/tapi_yandex_metrika/resource_mapping.py
@@ -1,4 +1,3 @@
-# coding: utf-8
STATS_RESOURCE_MAPPING = {
"stats": {
@@ -88,7 +87,8 @@
& [sort=]
& [status=]
& [type=]""",
- "methods": ["GET", "POST"]
+ "methods": ["GET", "POST"],
+ "response_data_key": "counters",
},
"counter": {
"resource": "management/v1/counter/{counterId}",
@@ -106,7 +106,8 @@
"resource": "management/v1/counter/{counterId}/goals",
"docs": "https://yandex.ru/dev/metrika/doc/api2/management/goals/goals-docpage/",
"params": """[callback=] & [useDeleted=]""",
- "methods": ["GET", "POST"]
+ "methods": ["GET", "POST"],
+ "response_data_key": "goals",
},
"goal": {
"resource": "management/v1/counter/{counterId}/goal/{goalId}",
@@ -118,13 +119,15 @@
"resource": "management/v1/accounts",
"docs": "https://yandex.ru/dev/metrika/doc/api2/management/accounts/accounts-docpage/",
"params": """[callback=] & [user_login=]""",
- "methods": ["GET", "DELETE", "PUT"]
+ "methods": ["GET", "DELETE", "PUT"],
+ "response_data_key": "accounts",
},
"clients": {
"resource": "management/v1/clients",
"docs": "https://yandex.ru/dev/metrika/doc/api2/management/direct_clients/getclients-docpage/",
"params": """counters=""",
- "methods": ["GET", ]
+ "methods": ["GET", ],
+ "response_data_key": "clients",
},
"filters": {
"resource": "management/v1/counter/{counterId}/filters",
diff --git a/tapi_yandex_metrika/tapi_yandex_metrika.py b/tapi_yandex_metrika/tapi_yandex_metrika.py
index 376e838..6c04f80 100644
--- a/tapi_yandex_metrika/tapi_yandex_metrika.py
+++ b/tapi_yandex_metrika/tapi_yandex_metrika.py
@@ -1,13 +1,12 @@
-# coding: utf-8
-import json
+import io
import logging
import random
import re
import time
-import simplejson
-from tapi import TapiAdapter, generate_wrapper_from_adapter, JSONAdapterMixin
-from tapi.exceptions import ResponseProcessException, ClientError
+import orjson
+from tapi2 import TapiAdapter, generate_wrapper_from_adapter, JSONAdapterMixin
+from tapi2.exceptions import ResponseProcessException
from tapi_yandex_metrika import exceptions
from .resource_mapping import (
@@ -16,22 +15,19 @@
MANAGEMENT_RESOURCE_MAPPING,
)
-logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+LIMIT = 10000
-class YandexMetrikaClientAdapterAbstract(JSONAdapterMixin, TapiAdapter):
- resource_mapping = NotImplementedError # карта ресурсов
- def get_api_root(self, api_params):
+class YandexMetrikaClientAdapterAbstract(JSONAdapterMixin, TapiAdapter):
+ def get_api_root(self, api_params, resource_name):
return "https://api-metrika.yandex.net/"
def get_request_kwargs(self, api_params, *args, **kwargs):
- """
- Обогащение запроса, параметрами.
+ if "receive_all_data" in api_params:
+ raise exceptions.BackwardCompatibilityError("parameter 'receive_all_data'")
- :param api_params: dict
- :return: dict
- """
params = super().get_request_kwargs(api_params, *args, **kwargs)
params["headers"]["Authorization"] = "OAuth {}".format(
api_params["access_token"]
@@ -39,324 +35,370 @@ def get_request_kwargs(self, api_params, *args, **kwargs):
return params
def get_error_message(self, data, response=None):
- """Извлечение комментария к ошибке запроса."""
- try:
- if not data and response.content.strip():
- data = json.loads(response.content.decode("utf-8"))
-
- if data:
- return data.get("message")
- except (json.JSONDecodeError, simplejson.JSONDecodeError):
- return response.text
-
- def process_response(self, response, **request_kwargs):
- """Обработка ответа запроса."""
- data = self.response_to_native(response)
-
- if isinstance(data, dict) and data.get("errors"):
- raise ResponseProcessException(ClientError, data)
+ if data is None:
+ return {"error_text": response.content.decode()}
else:
- # Дополнительная обработка происходит в методе родительского класса.
- data = super().process_response(response)
+ return data
+ def format_data_to_request(self, data):
+ if data:
+ return orjson.dumps(data)
+
+ def process_response(self, response, request_kwargs, **kwargs):
+ data = super().process_response(response, request_kwargs, **kwargs)
+ if isinstance(data, dict) and "errors" in data:
+ raise ResponseProcessException(response, data)
return data
def response_to_native(self, response):
- """Преобразование ответа."""
if response.content.strip():
try:
- return response.json()
- except (json.JSONDecodeError, simplejson.JSONDecodeError):
+ return orjson.loads(response.content.decode())
+ except ValueError:
return response.text
- def wrapper_call_exception(
- self, response, tapi_exception, api_params, *args, **kwargs
- ):
- """
- Для вызова кастомных исключений.
- Когда например сервер отвечает 200,
- а ошибки передаются внутри json.
- """
- try:
- jdata = response.json()
- except (json.JSONDecodeError, simplejson.JSONDecodeError):
- raise exceptions.YandexMetrikaApiError(response)
- else:
- error_code = int(jdata.get("code", 0))
- message = jdata.get("message")
-
- if error_code == 429:
- raise exceptions.YandexMetrikaLimitError(response)
- elif error_code == 403:
- raise exceptions.YandexMetrikaTokenError(response)
- elif message == "Incorrect part number" and api_params.get(
- "receive_all_data", False
- ):
- # Срабатывает при попытке скачать несуществующую часть отчета.
- # А при получении всех частей отчета автоматически,
- # всегда идет попытка получить следующий часть.
- pass
- else:
- raise exceptions.YandexMetrikaClientError(response)
-
def retry_request(
- self, response, tapi_exception, api_params, count_request_error, *args, **kwargs
+ self,
+ tapi_exception,
+ error_message,
+ repeat_number,
+ response,
+ request_kwargs,
+ api_params,
+ **kwargs
):
- """
- Условия повторения запроса.
- Если вернет True, то запрос повторится.
-
- response = tapi_exception.client().response
- status_code = tapi_exception.client().status_code
- response_data = tapi_exception.client().data
- """
- status_code = tapi_exception.client().status_code
- response_data = tapi_exception.client().data or {}
- error_code = int(response_data.get("code", 0))
- errors_types = [i.get("error_type") for i in response_data.get("errors", [])]
+ code = int(error_message.get("code", 0))
+ message = error_message.get("message", "")
+ errors_types = [i.get("error_type") for i in error_message.get("errors", [])]
limit_errors = {
- "quota_requests_by_uid": "Превышен лимит количества запросов к API в сутки для пользователя.",
- "quota_delegate_requests": "Превышен лимит количества запросов к API на добавление представителей в час для пользователя.",
- "quota_grants_requests": "Превышен лимит количества запросов к API на добавление доступов к счетчику в час",
- "quota_requests_by_ip": "Превышен лимит количества запросов к API в секунду для IP адреса.",
- "quota_parallel_requests": "Превышен лимит количества параллельных запросов к API в сутки для пользователя.",
- "quota_requests_by_counter_id": "Превышен лимит количества запросов к API в сутки для счётчика.",
+ "quota_requests_by_uid":
+ "The limit on the number of API requests per day for the user has been exceeded.",
+ "quota_delegate_requests":
+ "Exceeded the limit on the number of API requests to add representatives per hour for a user.",
+ "quota_grants_requests":
+ "Exceeded the limit on the number of API requests to add access to the counter per hour",
+ "quota_requests_by_ip":
+ "The limit on the number of API requests per second for an IP address has been exceeded.",
+ "quota_parallel_requests":
+ "The limit on the number of parallel API requests per day for the user has been exceeded.",
+ "quota_requests_by_counter_id":
+ "The limit on the number of API requests per day for the counter has been exceeded.",
}
+ big_report_request = (
+ "Query is too complicated. Please reduce the date interval or sampling."
+ )
- if error_code == 429:
+ if code == 400:
+ if message == big_report_request:
+ if repeat_number < 10:
+ retry_seconds = random.randint(5, 30)
+ big_report_request += " Re-request after {} seconds".format(
+ retry_seconds
+ )
+ logger.warning(big_report_request)
+ time.sleep(retry_seconds)
+ return True
+
+ if code == 429:
if "quota_requests_by_ip" in errors_types:
retry_seconds = random.randint(1, 30)
- error_text = "{}\nПовторный запрос через {} сек.".format(
+ error_text = "{} Re-request after {} seconds.".format(
limit_errors["quota_requests_by_ip"], retry_seconds
)
- logging.warning(error_text)
+ logger.warning(error_text)
time.sleep(retry_seconds)
return True
else:
for err in errors_types:
- logging.error(limit_errors[err])
+ logger.error(limit_errors[err])
- elif error_code == 503:
- if count_request_error < api_params.get("retries_if_server_error", 3):
- logging.warning("Серверная ошибка. Повторный запрос через 3 секунды")
- time.sleep(3)
+ elif code == 503:
+ if repeat_number < api_params.get("retries_if_server_error", 3):
+ logger.warning("Server error. Re-request after 3 seconds")
+ time.sleep(5)
return True
return False
+ def error_handling(
+ self,
+ tapi_exception,
+ error_message,
+ repeat_number,
+ response,
+ request_kwargs,
+ api_params,
+ **kwargs
+ ):
+ if error_message.get("error_text"):
+ raise exceptions.YandexMetrikaApiError(response)
+ else:
+ error_code = int(error_message.get("code", 0))
-class YandexMetrikaManagementClientAdapter(YandexMetrikaClientAdapterAbstract):
- resource_mapping = MANAGEMENT_RESOURCE_MAPPING # карта ресурсов
+ if error_code == 429:
+ raise exceptions.YandexMetrikaLimitError(response, **error_message)
+ elif error_code == 403:
+ raise exceptions.YandexMetrikaTokenError(response, **error_message)
+ else:
+ raise exceptions.YandexMetrikaClientError(response, **error_message)
- def transform_results(self, results, requests_kwargs, responses, api_params):
- """
- Преобразователь данных после получения всех ответов.
+ def transform(self, **kwargs):
+ raise exceptions.BackwardCompatibilityError("method 'transform'")
- :param results: list : данные всех запросов
- :param requests_kwargs: list : параметры всех запросов
- :param responses: list : ответы всех запросов
- :param api_params: dict : входящие параметры класса
- :return: list
- """
- return results[0] if isinstance(results, list) and results else results
+
+class YandexMetrikaManagementClientAdapter(YandexMetrikaClientAdapterAbstract):
+ resource_mapping = MANAGEMENT_RESOURCE_MAPPING
class YandexMetrikaLogsapiClientAdapter(YandexMetrikaClientAdapterAbstract):
resource_mapping = LOGSAPI_RESOURCE_MAPPING
- def transform_results(self, results, requests_kwargs, responses, api_params):
- """
- Преобразователь данных после получения всех ответов.
-
- :param results: list : данные всех запросов
- :param requests_kwargs: list : параметры всех запросов
- :param responses: list : ответы всех запросов
- :param api_params: dict : входящие параметры класса
- :return: list
- """
- if (
- api_params.get("receive_all_data", False)
- and responses[0].url.find("download") > -1
- ):
- # Собирает все части отчета в один.
- data, cols = "", ""
- for i in results:
- cols = i[: i.find("\n")] # строка с именами столбцов
- # Данные без строки со столбцами.
- data += i[i.find("\n") + 1 :]
- return "{}\n{}".format(cols, data)
- else:
- return results[0] if isinstance(results, list) and results else results
-
- def transform(self, data, request_kwargs, response, api_params, *args, **kwargs):
- """Преобразование данных."""
- if response.url.find("download") > -1:
- json_data = [
- i.split("\t")
- for i in data.split("\n")
- if i != "" # удаляется пустая последняя строка
- ]
- return json_data
+ def process_response(self, response, request_kwargs, **kwargs):
+ data = super().process_response(response, request_kwargs, **kwargs)
+ if "download" in request_kwargs["url"]:
+ kwargs["store"]["columns"] = data[: data.find("\n")].split("\t")
else:
- raise NotImplementedError(
- "Преобразование в JSON доступно только для ответов ресурса download"
- )
+ kwargs["store"].pop("columns", None)
+ return data
+
+ def error_handling(
+ self,
+ tapi_exception,
+ error_message,
+ repeat_number,
+ response,
+ request_kwargs,
+ api_params,
+ **kwargs
+ ):
+ message = error_message.get("message")
+ if message == "Incorrect part number":
+ # Fires when trying to download a non-existent part of a report.
+ return
+
+ if message == "Only log of requests in status 'processed' can be downloaded":
+ raise exceptions.YandexMetrikaDownloadReportError(response)
+
+ return super().error_handling(
+ tapi_exception,
+ error_message,
+ repeat_number,
+ response,
+ request_kwargs,
+ api_params,
+ **kwargs
+ )
+
+ def _is_exists_report(self, response, api_params, **kwargs):
+ request_id = api_params["default_url_params"]["requestId"]
+ if kwargs["store"].get(request_id) is None:
+ client = kwargs["client"]
+ info = client.info(requestId=request_id).get()
+ status = info.data["log_request"]["status"]
+ if "cleaned" in status:
+ raise exceptions.YandexMetrikaDownloadReportError(
+ response,
+ message="The report does not exist, it has been cleared. "
+ "Curent report status is '{}'".format(status),
+ )
+ kwargs["store"][request_id] = "exists"
def retry_request(
- self, response, tapi_exception, api_params, count_request_error, *args, **kwargs
+ self,
+ tapi_exception,
+ error_message,
+ repeat_number,
+ response,
+ request_kwargs,
+ api_params,
+ **kwargs
):
"""
- Условия повторения запроса.
- Если вернет True, то запрос повторится.
-
- response = tapi_exception.client().response
- status_code = tapi_exception.client().status_code
- response_data = tapi_exception.client().data
+ Conditions for repeating a request. If it returns True, the request will be repeated.
"""
- status_code = tapi_exception.client().status_code
- response_data = tapi_exception.client().data or {}
- error_code = int((response_data).get("code", 0))
- message = response_data.get("message")
+ message = error_message.get("message")
if (
message == "Only log of requests in status 'processed' can be downloaded"
+ and "download" in request_kwargs["url"]
and api_params.get("wait_report", False)
- and response.url.find("download") > -1
):
- # Ошибка появляется при попытке скачать неготовый отчет.
- sleep_time = count_request_error * 20
- logging.info(
- "Включен режим ожидания готовности отчета. "
- "Проверка готовности отчета через {} сек.".format(sleep_time)
- )
+ self._is_exists_report(response, api_params, **kwargs)
+
+ # The error appears when trying to download an unprepared report.
+ max_sleep = 60 * 5
+ sleep_time = repeat_number * 60
+ sleep_time = sleep_time if sleep_time <= max_sleep else max_sleep
+ logger.info("Wait report {} sec.".format(sleep_time))
time.sleep(sleep_time)
+
return True
return super().retry_request(
- response, tapi_exception, api_params, count_request_error, *args, **kwargs
+ tapi_exception,
+ error_message,
+ repeat_number,
+ response,
+ request_kwargs,
+ api_params,
+ **kwargs
)
- def extra_request(
- self,
- api_params,
- current_request_kwargs,
- request_kwargs_list,
- response,
- current_result,
+ def fill_resource_template_url(self, template, params, resource):
+ if resource == "download" and not params.get("partNumber"):
+ params.update(partNumber=0)
+ return super().fill_resource_template_url(template, params, resource)
+
+ def get_iterator_next_request_kwargs(
+ self, response_data, response, request_kwargs, api_params, **kwargs
):
- """
- Чтобы получить все части отчета,
- генерирует параметры для новых запросов к апи.
-
- Формирование дополнительных запросов.
- Они будут сделаны, если отсюда вернется
- непустой массив с набором параметров для запросов.
-
- :param api_params: dict : входящие параметры класса
- :param current_request_kwargs: dict : {headers, data, url, params} : параметры текущего запроса
- :param request_kwargs_list: list :
- Наборы параметров для запросов, которые будут сделаны.
- В него можно добавлять дополнительные наборы параметров, чтоб сделать дополнительные запросы.
- :param response: request_object : текущий ответ
- :param current_result: json : текущий результат
- :return: list : request_kwargs_list
- """
- # request_kwargs_list может содержать наборы параметров запросов, которые еще не сделаны.
- # Поэтому в него нужно добавлять новые, а не заменять.
- if (
- api_params.get("receive_all_data", False)
- and response.url.find("download") > -1
- ):
- url = current_request_kwargs["url"]
- part = int(re.findall(r"part/([0-9]*)/", url)[0])
- new_part = part + 1
- logging.info(
- "Включен режим получения всех данных. "
- "Запрашиваю следующую часть отчета: {}".format(new_part)
- )
- new_url = re.sub(r"part/[0-9]*/", "part/{}/".format(new_part), url)
- new_request_kwargs = {**current_request_kwargs, "url": new_url}
- request_kwargs_list.append(new_request_kwargs)
-
- return request_kwargs_list
-
- def fill_resource_template_url(self, template, params):
- """
- Заполнение параметрами, адреса ресурса из которого формируется URL.
+ url = request_kwargs["url"]
- :param template: str : ресурс
- :param params: dict : параметры
- :return:
- """
- if template.find("/part/") > -1 and not params.get("partNumber"):
- # Принудительно добавляет параметр partNumber, если его нет.
- params.update(partNumber=0)
- return template.format(**params)
+ if "download" not in url:
+ raise NotImplementedError("Iteration not supported for this resource")
+
+ part = int(re.findall(r"part/([0-9]*)/", url)[0])
+ next_part = part + 1
+ new_url = re.sub(r"part/[0-9]*/", "part/{}/".format(next_part), url)
+ return {**request_kwargs, "url": new_url}
+
+ def _iter_line(self, text, **kwargs):
+ if "download" not in kwargs["request_kwargs"]["url"]:
+ raise NotImplementedError("Only available for download resource responses")
+
+ f = io.StringIO(text)
+ next(f) # skipping columns
+ return (line.replace("\n", "") for line in f)
+
+ def get_iterator_iteritems(self, response_data, **kwargs):
+ if response_data:
+ return self._iter_line(response_data, **kwargs)
+ else:
+ return []
+
+ def get_iterator_pages(self, response_data, **kwargs):
+ if response_data:
+ return [response_data]
+ else:
+ return []
+
+ def get_iterator_items(self, data, **kwargs):
+ return self._iter_line(data, **kwargs)
+
+ def parts(self, max_parts=None, **kwargs):
+ client = kwargs["client"]
+ yield from client.pages(max_pages=max_parts)
+
+ def iter_lines(self, max_parts=None, max_items=None, **kwargs):
+ client = kwargs["client"]
+ yield from client.iter_items(max_pages=max_parts, max_items=max_items)
+
+ def iter_values(self, max_parts=None, max_items=None, **kwargs):
+ client = kwargs["client"]
+ for line in client.iter_items(max_pages=max_parts, max_items=max_items):
+ yield line.split("\t")
+
+ def lines(self, max_items=None, **kwargs):
+ client = kwargs["client"]
+ yield from client.items(max_items=max_items)
+
+ def values(self, max_items=None, **kwargs):
+ client = kwargs["client"]
+ for line in client.items(max_items=max_items):
+ yield line.split("\t")
+
+ def to_values(self, data, **kwargs):
+ return [line.split("\t") for line in data.split("\n")[1:] if line]
+
+ def to_lines(self, data, **kwargs):
+ return [line for line in data.split("\n")[1:] if line]
+
+ def to_columns(self, data, **kwargs):
+ columns = [[] for _ in range(len(kwargs["store"]["columns"]))]
+ for line in self._iter_line(data, **kwargs):
+ values = line.split("\t")
+ for i, col in enumerate(columns):
+ col.append(values[i])
+
+ return columns
class YandexMetrikaStatsClientAdapter(YandexMetrikaClientAdapterAbstract):
resource_mapping = STATS_RESOURCE_MAPPING
- def extra_request(
- self,
- api_params,
- current_request_kwargs,
- request_kwargs_list,
- response,
- current_result,
- ):
- """
- Чтобы получить все строки отчета,
- генерирует параметры для новых запросов к апи.
-
- Формирование дополнительных запросов.
- Они будут сделаны, если отсюда вернется
- непустой массив с набором параметров для запросов.
-
- :param api_params: dict : входящие параметры класса
- :param current_request_kwargs: dict : {headers, data, url, params} : параметры текущего запроса
- :param request_kwargs_list: list :
- Наборы параметров для запросов, которые будут сделаны.
- В него можно добавлять дополнительные наборы параметров, чтоб сделать дополнительные запросы.
- :param response: request_object : текущий ответ
- :param current_result: json : текущий результат
- :return: list : request_kwargs_list
- """
- # request_kwargs_list может содержать наборы параметров запросов, которые еще не сделаны.
- # Поэтому в него нужно добавлять новые, а не заменять.
- total_rows = int(current_result["total_rows"])
- sampled = current_result["sampled"]
+ def process_response(self, response, request_kwargs, **kwargs):
+ data = super().process_response(response, request_kwargs, **kwargs)
+ attribution = data["query"]["attribution"]
+ sampled = data["sampled"]
+ sample_share = data["sample_share"]
+ total_rows = int(data["total_rows"])
+ offset = data["query"]["offset"]
+ limit = request_kwargs["params"].get("limit", LIMIT)
+ offset2 = offset + limit - 1
+ if offset2 > total_rows:
+ offset2 = total_rows
+
+ if sampled:
+ logger.info("Sample: {}".format(sample_share))
+ logger.info("Attribution: {}".format(attribution))
+ logger.info(
+ "Exported lines {}-{}. Total rows {}".format(offset, offset2, total_rows)
+ )
- logging.info("Наличие семплирования: " + str(sampled))
- limit = current_request_kwargs["params"].get("limit", 10000)
- offset = current_result["query"]["offset"] + limit
+ kwargs["store"]["columns"] = (
+ data["query"]["dimensions"] + data["query"]["metrics"]
+ )
+
+ return data
+
+ def _iter_transform_data(self, data):
+ for row in data["data"]:
+ dimensions_data = [i["name"] for i in row["dimensions"]]
+ metrics_data = row["metrics"]
+ yield dimensions_data + metrics_data
+
+ def to_values(self, data, **kwargs):
+ return list(self._iter_transform_data(data))
+
+ def to_columns(self, data, **kwargs):
+ columns = None
+ for row in self._iter_transform_data(data):
+ if columns is None:
+ columns = [[] for _ in range(len(row))]
+
+ for i, col in enumerate(columns):
+ col.append(row[i])
+
+ return columns
+
+ def get_iterator_next_request_kwargs(
+ self, response_data, response, request_kwargs, api_params, **kwargs
+ ):
+ total_rows = int(response_data["total_rows"])
+ limit = request_kwargs["params"].get("limit", LIMIT)
+ offset = response_data["query"]["offset"] + limit
if offset <= total_rows:
- logging.info(
- "Получено строк {}. Всего строк {}".format(offset - 1, total_rows)
- )
- if api_params.get("receive_all_data", False):
- logging.info(
- "Включен режим получения всех данных. "
- "Запрашиваю следующие части отчета."
- )
- new_request_kwargs = current_request_kwargs.copy()
- new_request_kwargs["params"]["offset"] = offset
- request_kwargs_list.append(new_request_kwargs)
- return request_kwargs_list
-
- def transform(self, data, request_kwargs, response, api_params, *args, **kwargs):
- """Преобразование данных."""
- new_data = []
- columns = data[0]["query"]["dimensions"] + data[0]["query"]["metrics"]
- for result in data:
- data = result.pop("data")
- for row in data:
- dimensions = [i["name"] for i in row["dimensions"]]
- metrics = row["metrics"]
- new_data.append(dimensions + metrics)
- return [columns] + new_data
+ request_kwargs["params"]["offset"] = offset
+ return request_kwargs
+
+ def get_iterator_iteritems(self, response_data, **kwargs):
+ return self._iter_transform_data(response_data)
+
+ def get_iterator_pages(self, response_data, **kwargs):
+ return [response_data]
+
+ def get_iterator_items(self, data, **kwargs):
+ return self._iter_transform_data(data)
+
+ def iter_rows(self, max_pages=None, max_items=None, **kwargs):
+ client = kwargs["client"]
+ yield from client.iter_items(max_pages=max_pages, max_items=max_items)
+
+ def rows(self, max_items=None, **kwargs):
+ client = kwargs["client"]
+ yield from client.items(max_items=max_items)
YandexMetrikaStats = generate_wrapper_from_adapter(YandexMetrikaStatsClientAdapter)
diff --git a/tests/losgapi_tests.py b/tests/losgapi_tests.py
index c140dbc..f25c4f8 100644
--- a/tests/losgapi_tests.py
+++ b/tests/losgapi_tests.py
@@ -1,13 +1,11 @@
-# coding: utf-8
-import logging
+import mock
+import requests
import time
-
import yaml
+from tapi2.adapters import TapiAdapter
from tapi_yandex_metrika import YandexMetrikaLogsapi
-logging.basicConfig(level=logging.DEBUG)
-
with open("../config.yml", "r") as stream:
data_loaded = yaml.safe_load(stream)
@@ -18,7 +16,6 @@
access_token=ACCESS_TOKEN,
default_url_params={"counterId": COUNTER_ID},
wait_report=True,
- receive_all_data=True,
)
@@ -32,8 +29,8 @@ def test_evaluate():
params={
"fields": "ym:s:date",
"source": "visits",
- "date1": "2019-01-01",
- "date2": "2019-01-01",
+ "date1": "2020-12-01",
+ "date2": "2020-12-01",
}
)
print(r)
@@ -42,33 +39,51 @@ def test_evaluate():
def test_create():
r = api.create().post(
params={
- "fields": "ym:s:bounce",
+ "fields": "ym:s:date",
+ # "fields": "ym:s:purchaseID,ym:s:purchaseDateTime,ym:s:purchaseAffiliation,ym:s:purchaseRevenue,ym:s:purchaseTax,ym:s:purchaseShipping,ym:s:purchaseCoupon,ym:s:purchaseCurrency,ym:s:purchaseProductQuantity,ym:s:productsPurchaseID,ym:s:productsID,ym:s:productsName,ym:s:productsBrand,ym:s:productsCategory,ym:s:productsCategory1,ym:s:productsCategory2,ym:s:productsCategory3,ym:s:productsCategory4,ym:s:productsCategory5,ym:s:productsVariant,ym:s:productsPosition,ym:s:productsPrice,ym:s:productsCurrency,ym:s:productsCoupon,ym:s:productsQuantity,ym:s:impressionsURL,ym:s:impressionsDateTime,ym:s:impressionsProductID,ym:s:impressionsProductName,ym:s:impressionsProductBrand,ym:s:impressionsProductCategory,ym:s:impressionsProductCategory1,ym:s:impressionsProductCategory2,ym:s:impressionsProductCategory3,ym:s:impressionsProductCategory4,ym:s:impressionsProductCategory5,ym:s:impressionsProductVariant,ym:s:impressionsProductPrice,ym:s:impressionsProductCurrency,ym:s:impressionsProductCoupon,ym:s:counterID,ym:s:clientID,ym:s:visitID,ym:s:watchIDs,ym:s:date,ym:s:dateTime,ym:s:dateTimeUTC,ym:s:clientTimeZone,ym:s:bounce,ym:s:pageViews,ym:s:visitDuration,ym:s:isNewUser,ym:s:startURL,ym:s:endURL,ym:s:UTMCampaign,ym:s:UTMContent,ym:s:UTMMedium,ym:s:UTMSource,ym:s:UTMTerm,ym:s:lastTrafficSource,ym:s:lastAdvEngine,ym:s:lastReferalSource,ym:s:lastSearchEngineRoot,ym:s:lastSearchEngine,ym:s:lastSocialNetwork,ym:s:lastSocialNetworkProfile,ym:s:referer,ym:s:hasGCLID,ym:s:lastDirectClickOrder,ym:s:lastDirectBannerGroup,ym:s:lastDirectClickBanner,ym:s:lastDirectPhraseOrCond,ym:s:lastDirectPlatformType,ym:s:lastDirectPlatform,ym:s:lastDirectConditionType,ym:s:lastCurrencyID,ym:s:regionCountry,ym:s:regionCity,ym:s:deviceCategory,ym:s:mobilePhone,ym:s:mobilePhoneModel,ym:s:goalsID,ym:s:goalsSerialNumber,ym:s:goalsDateTime,ym:s:goalsPrice,ym:s:goalsOrder",
"source": "visits",
- "date1": "2020-04-17",
- "date2": "2020-04-17",
+ "date1": "2020-12-01",
+ "date2": "2020-12-01",
}
)
global request_id
- request_id = r().data["log_request"]["request_id"]
+ request_id = r["log_request"]["request_id"]
print(r)
+ return request_id
-def test_get_info():
- r = api.info(requestId=request_id).get()
- print(r)
+def test_download():
+ report = api.download(requestId=request_id).get()
+ for part in report().parts(max_parts=2):
+ for line in part().lines(max_items=2):
+ print('line', line)
+ print("part", part().to_values()[:1])
+ print("part", part().to_lines()[:1])
-def test_cancel():
- r = api.cancel(requestId=request_id).post()
+ print("report", report().to_values()[:1])
+ print("report", report().to_lines()[:1])
+
+
+def test_iter():
+ report = api.download(requestId=request_id).get()
+ for line in report().iter_lines(max_items=2):
+ print('line', line)
+
+ for values in report().iter_values(max_items=2):
+ print('values', values)
+
+
+def test_get_info():
+ r = api.info(requestId=request_id).get()
print(r)
def test_clean():
- test_create()
-
+ request_id = test_create()
while True:
- r_ = api.info(requestId=request_id).get()
- if r_().data["log_request"]["status"] == "processed":
+ r = api.info(requestId=request_id).get()
+ if r.data["log_request"]["status"] == "processed":
break
time.sleep(5)
@@ -76,8 +91,59 @@ def test_clean():
print(r)
-def test_download():
- test_create()
- r = api.download(requestId=request_id).get()
- data = r().transform()
- print("\n", len(data), "\n", data[:5])
+def test_cancel():
+ request_id = test_create()
+ r = api.cancel(requestId=request_id).post()
+ print(r)
+
+
+def test_transform():
+ requests.sessions.Session.request = mock.Mock(return_value=None)
+ response_data = (
+ "col1\tcol2\n"
+ "val1\tval2\n"
+ "val11\tval22\n"
+ "val111\tval222\n"
+ "val1111\tval2222\n"
+ )
+ TapiAdapter.process_response = mock.Mock(return_value=response_data)
+ report = api.download(requestId=None).get()
+
+ assert report.columns == ["col1", "col2"]
+ assert report().to_values() == [
+ ['val1', 'val2'], ['val11', 'val22'], ['val111', 'val222'], ['val1111', 'val2222']
+ ]
+ assert report().to_lines() == ['val1\tval2', 'val11\tval22', 'val111\tval222', 'val1111\tval2222']
+ assert report().to_columns() == [['val1', 'val11', 'val111', 'val1111'], ['val2', 'val22', 'val222', 'val2222']]
+
+
+def test_iteration():
+ requests.sessions.Session.request = mock.Mock(return_value=None)
+ response_data = (
+ "col1\tcol2\n"
+ "val1\tval2\n"
+ "val11\tval22\n"
+ "val111\tval222\n"
+ "val1111\tval2222\n"
+ )
+ expected_lines = response_data.split("\n")[1:]
+ expected_values = [i.split("\t") for i in response_data.split("\n")[1:]]
+
+ TapiAdapter.process_response = mock.Mock(return_value=response_data)
+ report = api.download(requestId=None).get()
+
+ for part in report().parts(max_parts=1):
+ assert 4 == len(list(part().lines()))
+ assert 4 == len(list(part().values()))
+
+ for line, expected in zip(part().lines(), expected_lines):
+ assert line == expected
+
+ for values, expected in zip(part().values(), expected_values):
+ assert values == expected
+
+ for line, expected in zip(report().iter_lines(max_items=3), expected_lines):
+ assert line == expected
+
+ for values, expected in zip(report().iter_values(max_items=3), expected_values[:4]):
+ assert values == expected
diff --git a/tests/management_tests.py b/tests/management_tests.py
index 50e82a7..84360b3 100644
--- a/tests/management_tests.py
+++ b/tests/management_tests.py
@@ -1,6 +1,4 @@
-# coding: utf-8
import logging
-
import yaml
from tapi_yandex_metrika import YandexMetrikaManagement
@@ -19,13 +17,12 @@
def test_info():
- api.clients().info()
+ print(dir(api))
def test_get_counters():
r = api.counters().get(params={"sort": "Visits"})
- print(r)
- print(r().data)
+ print(r.data)
def test_get_clients():
@@ -36,7 +33,7 @@ def test_get_clients():
def test_get_goals():
r = api.goals().get()
global goal_id
- goal_id = r().data["goals"][0]["id"]
+ goal_id = r.data["goals"][0]["id"]
print(r)
diff --git a/tests/stats_tests.py b/tests/stats_tests.py
index 3d6ef08..6346fa4 100644
--- a/tests/stats_tests.py
+++ b/tests/stats_tests.py
@@ -1,8 +1,9 @@
-# coding: utf-8
+import datetime as dt
import logging
-from pprint import pprint
-
+import mock
+import requests
import yaml
+from tapi2.adapters import TapiAdapter
from tapi_yandex_metrika import YandexMetrikaStats
@@ -14,28 +15,108 @@
ACCESS_TOKEN = data_loaded["token"]
COUNTER_ID = data_loaded["counter_id"]
-api = YandexMetrikaStats(access_token=ACCESS_TOKEN, receive_all_data=False)
+api = YandexMetrikaStats(access_token=ACCESS_TOKEN)
def test_info():
- api.stats().info()
+ api.stats().help()
-def test_get_stats():
+def test_stats():
params = dict(
ids=COUNTER_ID,
- metrics="ym:s:visits,ym:s:bounces",
- dimensions="ym:s:startOfHour",
- sort="ym:s:startOfHour",
- filters="ym:s:hour==10",
- date1="yesterday",
- date2="yesterday",
- group="hour",
- accuracy="full",
- attribution="lastsign",
- limit=10,
+ metrics="ym:s:visits",
+ filters="ym:s:startURL=.('https://rfgf.ru/map','https://rfgf.ru/map')",
+ limit=1,
)
r = api.stats().get(params=params)
- print(r().data)
- print("после преобразования\n")
- pprint(r().transform())
+ print(r.data)
+ print(r.response)
+
+
+def test_transform():
+ params = dict(
+ ids=100500,
+ metrics="ym:s:visits",
+ dimensions="ym:s:date",
+ sort="ym:s:date",
+ group='Day',
+ date1=dt.date(2020, 10, 1),
+ date2='2020-10-05',
+ limit=1,
+ )
+
+ requests.sessions.Session.request = mock.Mock(return_value=None)
+ response_data = {
+ 'query': {'ids': [100500], 'dimensions': ['ym:s:date'], 'metrics': ['ym:s:visits'], 'sort': ['ym:s:date'],
+ 'date1': '2020-10-01', 'date2': '2020-10-05', 'limit': 1, 'offset': 1, 'group': 'Day',
+ 'auto_group_size': '1', 'attr_name': '', 'quantile': '50', 'offline_window': '21',
+ 'attribution': 'LastSign', 'currency': 'RUB', 'adfox_event_id': '0'},
+ 'data': [{'dimensions': [{'name': '2020-10-01'}], 'metrics': [14234.0]},
+ {'dimensions': [{'name': '2020-10-02'}], 'metrics': [12508.0]},
+ {'dimensions': [{'name': '2020-10-03'}], 'metrics': [12365.0]},
+ {'dimensions': [{'name': '2020-10-04'}], 'metrics': [14588.0]},
+ {'dimensions': [{'name': '2020-10-05'}], 'metrics': [14579.0]}], 'total_rows': 5,
+ 'total_rows_rounded': False, 'sampled': False, 'contains_sensitive_data': False, 'sample_share': 1.0,
+ 'sample_size': 68280, 'sample_space': 68280, 'data_lag': 4242, 'totals': [68274.0], 'min': [12365.0],
+ 'max': [14588.0]
+ }
+ TapiAdapter.process_response = mock.Mock(return_value=response_data)
+
+ report = api.stats().get(params=params)
+
+ assert report.data == response_data
+ assert report().to_values() == [['2020-10-01', 14234.0],
+ ['2020-10-02', 12508.0],
+ ['2020-10-03', 12365.0],
+ ['2020-10-04', 14588.0],
+ ['2020-10-05', 14579.0]]
+ assert report().to_columns() == [['2020-10-01', '2020-10-02', '2020-10-03', '2020-10-04', '2020-10-05'],
+ [14234.0, 12508.0, 12365.0, 14588.0, 14579.0]]
+
+
+def test_iteration():
+ requests.sessions.Session.request = mock.Mock(return_value=None)
+ response_data = {
+ 'query': {
+ 'ids': [100500], 'dimensions': ['ym:s:date'], 'metrics': ['ym:s:visits'], 'sort': ['ym:s:date'],
+ 'date1': '2020-10-01', 'date2': '2020-10-05', 'limit': 1, 'offset': 1, 'group': 'Day',
+ 'auto_group_size': '1', 'attr_name': '', 'quantile': '50', 'offline_window': '21',
+ 'attribution': 'LastSign', 'currency': 'RUB', 'adfox_event_id': '0'
+ },
+ 'data': [{'dimensions': [{'name': '2020-10-01'}], 'metrics': [14234.0]},
+ {'dimensions': [{'name': '2020-10-02'}], 'metrics': [12508.0]}, ], 'total_rows': 5,
+ 'total_rows_rounded': False, 'sampled': False, 'contains_sensitive_data': False, 'sample_share': 1.0,
+ 'sample_size': 68280, 'sample_space': 68280, 'data_lag': 4242, 'totals': [68274.0], 'min': [12365.0],
+ 'max': [14588.0]
+ }
+ TapiAdapter.process_response = mock.Mock(return_value=response_data)
+
+ limit = 1
+ params = dict(
+ ids=100500,
+ metrics="ym:s:visits",
+ dimensions="ym:s:date",
+ sort="ym:s:date",
+ group='Day',
+ date1=dt.date(2020, 10, 1),
+ date2='2020-10-05',
+ limit=limit,
+ )
+ report = api.stats().get(params=params)
+
+ for page in report().pages():
+ assert page.data == response_data
+ for row in page().rows():
+ assert isinstance(row, list)
+ assert len(row) == 2
+
+ response_data["query"]["offset"] += limit
+ TapiAdapter.process_response = mock.Mock(return_value=response_data)
+
+ i = 0
+ for row in report().iter_rows():
+ i += 1
+ assert isinstance(row, list)
+ assert len(row) == 2
+ assert i == 2