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) +Code style: black -## Установка -``` -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