├── scripts ├── body-clients.json ├── body-report.json └── yandex_direct_export_to_file.py ├── tapi_yandex_direct ├── __init__.py ├── exceptions.py ├── resource_mapping.py ├── tapi_yandex_direct.pyi └── tapi_yandex_direct.py ├── .editorconfig ├── .gitignore ├── LICENSE ├── setup.py ├── tests ├── tests2.py └── tests.py ├── README.md └── examples.ipynb /scripts/body-clients.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "get", 3 | "params": { 4 | "FieldNames": [ 5 | "ClientId", 6 | "Login" 7 | ] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tapi_yandex_direct/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | __author__ = 'Pavel Maksimov' 3 | __email__ = 'vur21@ya.ru' 4 | __version__ = '2021.5.29' 5 | 6 | 7 | from .resource_mapping import * 8 | from .tapi_yandex_direct import YandexDirect 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [LICENSE] 18 | insert_final_newline = false 19 | 20 | [Makefile] 21 | indent_style = tab 22 | -------------------------------------------------------------------------------- /scripts/body-report.json: -------------------------------------------------------------------------------- 1 | { 2 | "params": { 3 | "SelectionCriteria": {}, 4 | "FieldNames": [ 5 | "CampaignType", 6 | "Clicks", 7 | "Cost" 8 | ], 9 | "OrderBy": [ 10 | { 11 | "Field": "CampaignType" 12 | } 13 | ], 14 | "ReportType": "ACCOUNT_PERFORMANCE_REPORT", 15 | "DateRangeType": "LAST_365_DAYS", 16 | "Format": "TSV", 17 | "IncludeVAT": "YES", 18 | "IncludeDiscount": "YES", 19 | "Page": { 20 | "Limit": 10 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | .pytest_cache 4 | venv 5 | .idea 6 | config.yml 7 | .pypirc 8 | .cache 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Packages 14 | *.egg 15 | *.egg-info 16 | .eggs/* 17 | dist 18 | build 19 | eggs 20 | parts 21 | bin 22 | var 23 | sdist 24 | develop-eggs 25 | .installed.cfg 26 | lib 27 | lib64 28 | 29 | # Installer logs 30 | pip-log.txt 31 | 32 | # Unit test / coverage reports 33 | .coverage 34 | .tox 35 | nosetests.xml 36 | htmlcov 37 | 38 | # Translations 39 | *.mo 40 | 41 | # Mr Developer 42 | .mr.developer.cfg 43 | .project 44 | .pydevproject 45 | 46 | # Complexity 47 | output/*.html 48 | output/*/index.html 49 | 50 | # Sphinx 51 | docs/_build 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Pavel Maksimov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | try: 4 | from setuptools import setup 5 | except ImportError: 6 | from distutils.core import setup 7 | 8 | import os 9 | import re 10 | 11 | with open("README.md", "r", encoding="utf8") as fh: 12 | readme = fh.read() 13 | 14 | package = "tapi_yandex_direct" 15 | 16 | 17 | def get_version(package): 18 | """ 19 | Return package version as listed in `__version__` in `init.py`. 20 | """ 21 | init_py = open(os.path.join(package, "__init__.py")).read() 22 | return re.search("^__version__ = ['\"]([^'\"]+)['\"]", init_py, re.MULTILINE).group( 23 | 1 24 | ) 25 | 26 | 27 | setup( 28 | name="tapi-yandex-direct", 29 | version=get_version(package), 30 | description="Python client for API Yandex Direct", 31 | long_description=readme, 32 | long_description_content_type="text/markdown", 33 | author="Pavel Maksimov", 34 | author_email="vur21@ya.ru", 35 | url="https://github.com/pavelmaksimov/tapi-yandex-direct", 36 | packages=[package], 37 | include_package_data=False, 38 | install_requires=["requests", "orjson", "tapi-wrapper2>=0.1.2,<1.0"], 39 | license="MIT", 40 | zip_safe=False, 41 | keywords="tapi,wrapper,yandex,metrika,api,direct,яндекс,директ,апи", 42 | test_suite="tests", 43 | package_data={ 44 | package: ["*"], 45 | }, 46 | ) 47 | -------------------------------------------------------------------------------- /tapi_yandex_direct/exceptions.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Union 2 | 3 | from requests import Response 4 | from tapi2.tapi import TapiClient 5 | 6 | 7 | class YandexDirectApiError(Exception): 8 | def __init__( 9 | self, 10 | response: Response, 11 | data: Union[str, dict], 12 | client: TapiClient, 13 | *args, 14 | **kwargs 15 | ): 16 | self.response = response 17 | self.data = data 18 | self.client = client 19 | 20 | def __str__(self): 21 | return "{} {} {}\nHEADERS = {}\nURL = {}".format( 22 | self.response.status_code, 23 | self.response.reason, 24 | self.data or self.response.text, 25 | self.response.headers, 26 | self.response.url, 27 | ) 28 | 29 | 30 | class YandexDirectClientError(YandexDirectApiError): 31 | def __init__( 32 | self, 33 | response: Response, 34 | message: Dict[str, dict], 35 | client: TapiClient, 36 | *args, 37 | **kwargs 38 | ): 39 | self.error_code = message["error"]["error_code"] 40 | self.request_id = message["error"]["request_id"] 41 | self.error_string = message["error"]["error_string"] 42 | self.error_detail = message["error"]["error_detail"] 43 | super().__init__(response, message, client, *args, **kwargs) 44 | 45 | def __str__(self): 46 | text = "request_id={}, error_code={}, error_string={}, error_detail={}" 47 | 48 | return text.format( 49 | self.request_id, self.error_code, self.error_string, self.error_detail 50 | ) 51 | 52 | 53 | class YandexDirectTokenError(YandexDirectClientError): 54 | def __init__(self, *args, **kwargs): 55 | super().__init__(*args, **kwargs) 56 | 57 | 58 | class YandexDirectNotEnoughUnitsError(YandexDirectClientError): 59 | def __init__(self, *args, **kwargs): 60 | super().__init__(*args, **kwargs) 61 | 62 | 63 | class YandexDirectRequestsLimitError(YandexDirectClientError): 64 | def __init__(self, *args, **kwargs): 65 | super().__init__(*args, **kwargs) 66 | 67 | 68 | class BackwardCompatibilityError(Exception): 69 | def __init__(self, name): 70 | self.name = name 71 | 72 | def __str__(self): 73 | return ( 74 | "This {} is deprecated and not supported. " 75 | "Install a later version " 76 | "'pip install --upgrade tapi-yandex-direct==2020.12.15'. " 77 | "Info https://github.com/pavelmaksimov/tapi-yandex-direct" 78 | ).format(self.name) 79 | -------------------------------------------------------------------------------- /tests/tests2.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | import logging 3 | 4 | import yaml 5 | 6 | from tapi_yandex_direct import YandexDirect 7 | 8 | logging.basicConfig(level=logging.DEBUG) 9 | 10 | with open("../config.yml", "r") as stream: 11 | data_loaded = yaml.safe_load(stream) 12 | 13 | ACCESS_TOKEN = data_loaded["token"] 14 | CLIENT_ID = data_loaded["client_id"] 15 | 16 | api = YandexDirect( 17 | access_token=ACCESS_TOKEN, 18 | is_sandbox=False, 19 | retry_if_not_enough_units=False, 20 | retry_if_exceeded_limit=False, 21 | retries_if_server_error=5, 22 | # Параметры для ресурса Reports 23 | processing_mode="offline", 24 | wait_report=True, 25 | return_money_in_micros=True, 26 | skip_report_header=True, 27 | skip_column_header=False, 28 | skip_report_summary=True, 29 | ) 30 | 31 | 32 | # def test_get_debugtoken(): 33 | # api.debugtoken(client_id=CLIENT_ID).open_in_browser() 34 | 35 | 36 | def test_get_clients(): 37 | r = api.clients().post( 38 | data={ 39 | "method": "get", 40 | "params": { 41 | "FieldNames": ["ClientId", "Login"], 42 | }, 43 | } 44 | ) 45 | print(r) 46 | print(r().extract()) 47 | 48 | 49 | def test_get_campaigns(): 50 | campaigns = api.campaigns().post( 51 | data={ 52 | "method": "get", 53 | "params": { 54 | "SelectionCriteria": {"States": ["ON", "OFF"]}, 55 | "FieldNames": ["Id", "Name", "State", "Status", "Type"], 56 | }, 57 | "Page": {"Limit": 1000}, 58 | } 59 | ) 60 | print(campaigns) 61 | 62 | 63 | def test_method_add(): 64 | body = { 65 | "method": "add", 66 | "params": { 67 | "Campaigns": [ 68 | { 69 | "Name": "MyCampaignTest", 70 | "StartDate": str(dt.datetime.now().date()), 71 | "TextCampaign": { 72 | "BiddingStrategy": { 73 | "Search": {"BiddingStrategyType": "HIGHEST_POSITION"}, 74 | "Network": {"BiddingStrategyType": "SERVING_OFF"}, 75 | }, 76 | "Settings": [], 77 | }, 78 | } 79 | ] 80 | }, 81 | } 82 | r = api.campaigns().post(data=body) 83 | print(r().extract()) 84 | 85 | 86 | def test_get_report(): 87 | report = api.reports().post( 88 | data={ 89 | "params": { 90 | "SelectionCriteria": {}, 91 | "FieldNames": ["Date", "CampaignId", "Clicks", "Cost"], 92 | "OrderBy": [{"Field": "Date"}], 93 | "ReportName": "Actual Data11111", 94 | "ReportType": "CAMPAIGN_PERFORMANCE_REPORT", 95 | "DateRangeType": "LAST_7_DAYS", 96 | "Format": "TSV", 97 | "IncludeVAT": "YES", 98 | "IncludeDiscount": "YES", 99 | } 100 | } 101 | ) 102 | print(report.columns) 103 | print(report().to_values()) 104 | print(report().to_lines()) 105 | print(report().to_columns()) 106 | 107 | 108 | def test_get_report2(): 109 | for i in range(7): 110 | r = api.reports().post( 111 | data={ 112 | "params": { 113 | "SelectionCriteria": {}, 114 | "FieldNames": ["Date", "CampaignId", "Clicks", "Cost"], 115 | "OrderBy": [{"Field": "Date"}], 116 | "ReportName": "Actual Data12 f 1" + str(i), 117 | "ReportType": "CAMPAIGN_PERFORMANCE_REPORT", 118 | "DateRangeType": "LAST_WEEK", 119 | "Format": "TSV", 120 | "IncludeVAT": "YES", 121 | "IncludeDiscount": "YES", 122 | } 123 | } 124 | ) 125 | print(r().response.status_code) 126 | -------------------------------------------------------------------------------- /tapi_yandex_direct/resource_mapping.py: -------------------------------------------------------------------------------- 1 | 2 | RESOURCE_MAPPING_V5 = { 3 | "adextensions": { 4 | "resource": "json/v5/adextensions", 5 | "docs": "https://tech.yandex.ru/direct/doc/ref-v5/adextensions/adextensions-docpage/", 6 | }, 7 | "adgroups": { 8 | "resource": "json/v5/adgroups", 9 | "docs": "https://tech.yandex.ru/direct/doc/ref-v5/adgroups/adgroups-docpage/", 10 | }, 11 | "adimages": { 12 | "resource": "json/v5/adimages", 13 | "docs": "https://tech.yandex.ru/direct/doc/ref-v5/adimages/adimages-docpage/", 14 | }, 15 | "ads": { 16 | "resource": "json/v5/ads", 17 | "docs": "https://tech.yandex.ru/direct/doc/ref-v5/ads/ads-docpage/", 18 | }, 19 | "agencyclients": { 20 | "resource": "json/v5/agencyclients", 21 | "docs": "https://tech.yandex.ru/direct/doc/ref-v5/agencyclients/agencyclients-docpage/", 22 | }, 23 | "audiencetargets": { 24 | "resource": "json/v5/audiencetargets", 25 | "docs": "https://tech.yandex.ru/direct/doc/ref-v5/audiencetargets/audiencetargets-docpage/", 26 | }, 27 | "bids": { 28 | "resource": "json/v5/bids", 29 | "docs": "https://tech.yandex.ru/direct/doc/ref-v5/bids/bids-docpage/", 30 | }, 31 | "bidmodifiers": { 32 | "resource": "json/v5/bidmodifiers", 33 | "docs": "https://tech.yandex.ru/direct/doc/ref-v5/bidmodifiers/bidmodifiers-docpage/", 34 | }, 35 | "campaigns": { 36 | "resource": "json/v5/campaigns", 37 | "docs": "https://tech.yandex.ru/direct/doc/ref-v5/campaigns/campaigns-docpage/", 38 | }, 39 | "changes": { 40 | "resource": "json/v5/changes", 41 | "docs": "https://tech.yandex.ru/direct/doc/ref-v5/changes/changes-docpage/", 42 | }, 43 | "clients": { 44 | "resource": "json/v5/clients", 45 | "docs": "https://tech.yandex.ru/direct/doc/ref-v5/clients/clients-docpage/", 46 | }, 47 | "creatives": { 48 | "resource": "json/v5/creatives", 49 | "docs": "https://tech.yandex.ru/direct/doc/ref-v5/creatives/creatives-docpage/", 50 | }, 51 | "dictionaries": { 52 | "resource": "json/v5/dictionaries", 53 | "docs": "https://tech.yandex.ru/direct/doc/ref-v5/dictionaries/dictionaries-docpage/", 54 | }, 55 | "dynamicads": { 56 | "resource": "json/v5/dynamictextadtargets", 57 | "docs": "https://tech.yandex.ru/direct/doc/ref-v5/dynamictextadtargets/dynamictextadtargets-docpage/", 58 | }, 59 | "keywordbids": { 60 | "resource": "json/v5/keywordbids", 61 | "docs": "https://tech.yandex.ru/direct/doc/ref-v5/keywordbids/keywordbids-docpage/", 62 | }, 63 | "keywords": { 64 | "resource": "json/v5/keywords", 65 | "docs": "https://tech.yandex.ru/direct/doc/ref-v5/keywords/keywords-docpage/", 66 | }, 67 | "keywordsresearch": { 68 | "resource": "json/v5/keywordsresearch", 69 | "docs": "https://tech.yandex.ru/direct/doc/ref-v5/keywordsresearch/keywordsresearch-docpage/", 70 | }, 71 | "leads": { 72 | "resource": "json/v5/leads", 73 | "docs": "https://tech.yandex.ru/direct/doc/ref-v5/leads/leads-docpage/", 74 | }, 75 | "retargeting": { 76 | "resource": "json/v5/retargetinglists", 77 | "docs": "https://tech.yandex.ru/direct/doc/ref-v5/retargetinglists/retargetinglists-docpage/", 78 | }, 79 | "sitelinks": { 80 | "resource": "json/v5/sitelinks", 81 | "docs": "https://tech.yandex.ru/direct/doc/ref-v5/sitelinks/sitelinks-docpage/", 82 | }, 83 | "vcards": { 84 | "resource": "json/v5/vcards", 85 | "docs": "https://tech.yandex.ru/direct/doc/ref-v5/vcards/vcards-docpage/", 86 | }, 87 | "turbopages": { 88 | "resource": "json/v5/turbopages", 89 | "docs": "https://yandex.ru/dev/direct/doc/ref-v5/turbopages/turbopages-docpage/", 90 | }, 91 | "negativekeywordsharedsets": { 92 | "resource": "json/v5/negativekeywordsharedsets", 93 | "docs": "https://yandex.ru/dev/direct/doc/ref-v5/negativekeywordsharedsets/negativekeywordsharedsets-docpage/", 94 | }, 95 | "reports": { 96 | "resource": "json/v5/reports", 97 | "docs": "https://yandex.ru/dev/direct/doc/reports/reports-docpage/", 98 | }, 99 | "debugtoken": { 100 | "resource": "oauth.yandex.ru/authorize?response_type=token&client_id={client_id}", 101 | "docs": "https://yandex.ru/dev/direct/doc/dg/concepts/auth-token-docpage/", 102 | }, 103 | "feeds": { 104 | "resource": "json/v5/feeds", 105 | "docs": "https://yandex.ru/dev/direct/doc/ref-v5/feeds/feeds.html", 106 | }, 107 | "smartadtargets": { 108 | "resource": "json/v5/smartadtargets", 109 | "docs": "https://yandex.ru/dev/direct/doc/ref-v5/smartadtargets/smartadtargets.html", 110 | }, 111 | "businesses": { 112 | "resource": "json/v5/businesses", 113 | "docs": "https://yandex.ru/dev/direct/doc/ref-v5/businesses/businesses.html", 114 | }, 115 | } 116 | -------------------------------------------------------------------------------- /tests/tests.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import responses 4 | 5 | from tapi_yandex_direct import YandexDirect 6 | 7 | logging.basicConfig(level=logging.DEBUG) 8 | 9 | client = YandexDirect( 10 | access_token="", 11 | is_sandbox=False, 12 | retry_if_not_enough_units=False, 13 | retry_if_exceeded_limit=False, 14 | retries_if_server_error=5, 15 | # For Reports resource. 16 | processing_mode="offline", 17 | wait_report=True, 18 | return_money_in_micros=True, 19 | skip_report_header=True, 20 | skip_column_header=False, 21 | skip_report_summary=True, 22 | ) 23 | 24 | 25 | @responses.activate 26 | def test_sanity(): 27 | responses.add( 28 | responses.POST, 29 | "https://api.direct.yandex.com/json/v5/clients", 30 | json={"result": {"Clients": []}}, 31 | status=200, 32 | ) 33 | 34 | result = client.clients().post( 35 | data={ 36 | "method": "get", 37 | "params": { 38 | "FieldNames": ["ClientId", "Login"], 39 | }, 40 | } 41 | ) 42 | assert result.data == {"result": {"Clients": []}} 43 | 44 | 45 | @responses.activate 46 | def test_extract(): 47 | responses.add( 48 | responses.POST, 49 | "https://api.direct.yandex.com/json/v5/clients", 50 | json={"result": {"Clients": []}}, 51 | status=200, 52 | ) 53 | 54 | result = client.clients().post( 55 | data={ 56 | "method": "get", 57 | "params": { 58 | "FieldNames": ["ClientId", "Login"], 59 | }, 60 | } 61 | ) 62 | assert result().extract() == [] 63 | 64 | 65 | @responses.activate 66 | def test_iter_items(): 67 | responses.add( 68 | responses.POST, 69 | "https://api.direct.yandex.com/json/v5/clients", 70 | json={"result": {"Clients": [{"id": 1}, {"id": 2}], "LimitedBy": 1}}, 71 | status=200, 72 | ) 73 | 74 | clients = client.clients().post( 75 | data={ 76 | "method": "get", 77 | "params": { 78 | "FieldNames": ["ClientId", "Login"], 79 | }, 80 | } 81 | ) 82 | 83 | ids = [] 84 | for item in clients().items(): 85 | ids.append(item["id"]) 86 | 87 | assert ids == [1, 2] 88 | 89 | 90 | @responses.activate 91 | def test_iter_pages_and_items(): 92 | responses.add( 93 | responses.POST, 94 | "https://api.direct.yandex.com/json/v5/clients", 95 | json={"result": {"Clients": [{"id": 1}], "LimitedBy": 1}}, 96 | status=200, 97 | ) 98 | responses.add( 99 | responses.POST, 100 | "https://api.direct.yandex.com/json/v5/clients", 101 | json={"result": {"Clients": [{"id": 2}]}}, 102 | status=200, 103 | ) 104 | 105 | clients = client.clients().post( 106 | data={ 107 | "method": "get", 108 | "params": { 109 | "FieldNames": ["ClientId", "Login"], 110 | }, 111 | } 112 | ) 113 | 114 | ids = [] 115 | for page in clients().pages(): 116 | for item in page().items(): 117 | ids.append(item["id"]) 118 | 119 | assert ids == [1, 2] 120 | 121 | 122 | @responses.activate 123 | def test_iter_items(): 124 | responses.add( 125 | responses.POST, 126 | "https://api.direct.yandex.com/json/v5/clients", 127 | json={"result": {"Clients": [{"id": 1}], "LimitedBy": 1}}, 128 | status=200, 129 | ) 130 | responses.add( 131 | responses.POST, 132 | "https://api.direct.yandex.com/json/v5/clients", 133 | json={"result": {"Clients": [{"id": 2}]}}, 134 | status=200, 135 | ) 136 | 137 | clients = client.clients().post( 138 | data={ 139 | "method": "get", 140 | "params": { 141 | "FieldNames": ["ClientId", "Login"], 142 | }, 143 | } 144 | ) 145 | 146 | ids = [] 147 | for item in clients().iter_items(): 148 | ids.append(item["id"]) 149 | 150 | assert ids == [1, 2] 151 | 152 | 153 | @responses.activate 154 | def test_get_report(): 155 | responses.add( 156 | responses.POST, 157 | "https://api.direct.yandex.com/json/v5/reports", 158 | headers={"retryIn": "0"}, 159 | status=202, 160 | ) 161 | responses.add( 162 | responses.POST, 163 | "https://api.direct.yandex.com/json/v5/reports", 164 | headers={"retryIn": "0"}, 165 | status=202, 166 | ) 167 | responses.add( 168 | responses.POST, 169 | "https://api.direct.yandex.com/json/v5/reports", 170 | body="col1\tcol2\nvalue1\tvalue2\nvalue10\tvalue20\n", 171 | status=200, 172 | ) 173 | report = client.reports().post( 174 | data={ 175 | "params": { 176 | "SelectionCriteria": {}, 177 | "FieldNames": ["Date", "CampaignId"], 178 | "OrderBy": [{"Field": "Date"}], 179 | "ReportName": "report name", 180 | "ReportType": "CAMPAIGN_PERFORMANCE_REPORT", 181 | "DateRangeType": "TODAY", 182 | "Format": "TSV", 183 | "IncludeVAT": "YES", 184 | "IncludeDiscount": "YES", 185 | } 186 | } 187 | ) 188 | assert report.columns == ["col1", "col2"] 189 | assert report().to_values() == [["value1", "value2"], ["value10", "value20"]] 190 | assert report().to_lines() == ["value1\tvalue2", "value10\tvalue20"] 191 | assert report().to_columns() == [["value1", "value10"], ["value2", "value20"]] 192 | assert report().to_dicts() == [ 193 | {"col1": "value1", "col2": "value2"}, 194 | {"col1": "value10", "col2": "value20"}, 195 | ] 196 | -------------------------------------------------------------------------------- /tapi_yandex_direct/tapi_yandex_direct.pyi: -------------------------------------------------------------------------------- 1 | from typing import List, Iterator, Union 2 | 3 | from requests import Response 4 | 5 | class YandexDirectBaseMethodsClientResponse: 6 | @property 7 | def data(self) -> dict: ... 8 | @property 9 | def request_kwargs(self) -> dict: ... 10 | @property 11 | def response(self) -> Response: ... 12 | @property 13 | def status_code(self) -> int: ... 14 | def __getitem__(self, item) -> Union[dict, list]: ... 15 | def __iter__(self) -> Iterator: ... 16 | 17 | # Yandex Direct management. 18 | class YandexDirectClientResponse(YandexDirectBaseMethodsClientResponse): 19 | def pages( 20 | self, *, max_pages: int = None 21 | ) -> Iterator["YandexDirectPageIteratorExecutor"]: ... 22 | def items(self, *, max_items: int = None) -> Iterator[dict]: ... 23 | def iter_items( 24 | self, *, max_pages: int = None, max_items: int = None 25 | ) -> Iterator[dict]: ... 26 | def extract(self) -> List[dict]: ... 27 | 28 | class YandexDirectClientExecutorResponse(YandexDirectBaseMethodsClientResponse): 29 | def __call__(self) -> YandexDirectClientResponse: ... 30 | 31 | class YandexDirectClientExecutor: 32 | def open_docs(self) -> YandexDirectClientExecutor: 33 | """Open API official docs of resource in browser.""" 34 | def open_in_browser(self) -> YandexDirectClientExecutor: 35 | """Send a request in the browser.""" 36 | def help(self) -> YandexDirectClientExecutor: 37 | """Print docs of resource.""" 38 | def get( 39 | self, *, params: dict = None, data: dict = None, headers: dict = None 40 | ) -> YandexDirectClientExecutorResponse: 41 | """ 42 | Send HTTP 'GET' request. 43 | 44 | :param params: querystring arguments in the URL 45 | :param data: send data in the body of the request 46 | """ 47 | def post( 48 | self, *, params: dict = None, data: dict = None, headers: dict = None 49 | ) -> YandexDirectClientExecutorResponse: 50 | """ 51 | Send HTTP 'POST' request. 52 | 53 | :param params: querystring arguments in the URL 54 | :param data: send data in the body of the request 55 | """ 56 | 57 | class YandexDirectPageIteratorResponse(YandexDirectBaseMethodsClientResponse): 58 | def items(self, *, max_items: int = None) -> Iterator[dict]: ... 59 | 60 | class YandexDirectPageIteratorExecutor(YandexDirectBaseMethodsClientResponse): 61 | def __call__(self) -> YandexDirectPageIteratorResponse: ... 62 | 63 | # Yandex Direct reports. 64 | class YandexDirectClientReportResponse(YandexDirectBaseMethodsClientResponse): 65 | def iter_lines(self) -> Iterator[str]: ... 66 | def iter_values(self) -> Iterator[list]: ... 67 | def iter_dicts(self) -> Iterator[dict]: ... 68 | def to_lines(self) -> List[str]: ... 69 | def to_values(self) -> List[list]: ... 70 | def to_columns(self) -> List[list]: ... 71 | def to_dicts(self) -> List[dict]: ... 72 | 73 | class YandexDirectClientReportExecutorResponse(YandexDirectBaseMethodsClientResponse): 74 | def __call__(self) -> YandexDirectClientReportResponse: ... 75 | @property 76 | def columns(self) -> List[str]: ... 77 | 78 | class YandexDirectClientReportExecutor: 79 | def open_docs(self) -> YandexDirectClientReportExecutor: 80 | """Open API official docs of resource in browser.""" 81 | def open_in_browser(self) -> YandexDirectClientReportExecutor: 82 | """Send a request in the browser.""" 83 | def help(self) -> YandexDirectClientReportExecutor: 84 | """Print docs of resource.""" 85 | def post( 86 | self, *, params: dict = None, data: dict = None, headers: dict = None 87 | ) -> YandexDirectClientReportExecutorResponse: 88 | """ 89 | Send HTTP 'POST' request. 90 | 91 | :param params: querystring arguments in the URL 92 | :param data: send data in the body of the request 93 | """ 94 | 95 | # Main. 96 | class YandexDirect: 97 | def __init__( 98 | self, 99 | *, 100 | access_token: str, 101 | login: str = None, 102 | is_sandbox: bool = False, 103 | retry_if_not_enough_units: bool = False, 104 | retry_if_exceeded_limit: bool = True, 105 | retries_if_server_error: int = 5, 106 | language: str = None, 107 | processing_mode: str = "offline", 108 | wait_report: bool = True, 109 | return_money_in_micros: bool = False, 110 | skip_report_header: bool = True, 111 | skip_column_header: bool = False, 112 | skip_report_summary: bool = True, 113 | ): 114 | """ 115 | Official documentation of the reports resource: https://yandex.ru/dev/direct/doc/ref-v5/concepts/about.html 116 | Official documentation of other resources: https://yandex.ru/dev/direct/doc/reports/how-to.html 117 | 118 | :param access_token: Access token. 119 | :param login: If you are making inquiries from an agent account, you must be sure to specify the account login. 120 | :param is_sandbox: Enable sandbox. 121 | :param retry_if_not_enough_units: Repeat request when units run out 122 | :param retry_if_exceeded_limit: Repeat the request if the limits on the number of reports or requests are exceeded. 123 | :param retries_if_server_error: Number of retries when server errors occur. 124 | :param language: The language in which the data for directories and errors will be returned. 125 | 126 | :param processing_mode: (report resource) Report generation mode: online, offline or auto. 127 | :param wait_report: (report resource) When requesting a report, it will wait until the report is prepared and download it. 128 | :param return_money_in_micros: (report resource) Monetary values in the report are returned in currency with an accuracy of two decimal places. 129 | :param skip_report_header: (report resource) Do not display a line with the report name and date range in the report. 130 | :param skip_column_header: (report resource) Do not display a line with field names in the report. 131 | :param skip_report_summary: (report resource) Do not display a line with the number of statistics lines in the report. 132 | """ 133 | def reports(self) -> YandexDirectClientReportExecutor: ... 134 | def adextensions(self) -> YandexDirectClientExecutor: ... 135 | def adgroups(self) -> YandexDirectClientExecutor: ... 136 | def adimages(self) -> YandexDirectClientExecutor: ... 137 | def ads(self) -> YandexDirectClientExecutor: ... 138 | def agencyclients(self) -> YandexDirectClientExecutor: ... 139 | def audiencetargets(self) -> YandexDirectClientExecutor: ... 140 | def bidmodifiers(self) -> YandexDirectClientExecutor: ... 141 | def bids(self) -> YandexDirectClientExecutor: ... 142 | def businesses(self) -> YandexDirectClientExecutor: ... 143 | def campaigns(self) -> YandexDirectClientExecutor: ... 144 | def changes(self) -> YandexDirectClientExecutor: ... 145 | def clients(self) -> YandexDirectClientExecutor: ... 146 | def creatives(self) -> YandexDirectClientExecutor: ... 147 | def debugtoken(self, *, client_id: str) -> YandexDirectClientExecutor: ... 148 | def dictionaries(self) -> YandexDirectClientExecutor: ... 149 | def dynamicads(self) -> YandexDirectClientExecutor: ... 150 | def feeds(self) -> YandexDirectClientExecutor: ... 151 | def keywordbids(self) -> YandexDirectClientExecutor: ... 152 | def keywords(self) -> YandexDirectClientExecutor: ... 153 | def keywordsresearch(self) -> YandexDirectClientExecutor: ... 154 | def leads(self) -> YandexDirectClientExecutor: ... 155 | def negativekeywordsharedsets(self) -> YandexDirectClientExecutor: ... 156 | def retargeting(self) -> YandexDirectClientExecutor: ... 157 | def sitelinks(self) -> YandexDirectClientExecutor: ... 158 | def smartadtargets(self) -> YandexDirectClientExecutor: ... 159 | def turbopages(self) -> YandexDirectClientExecutor: ... 160 | def vcards(self) -> YandexDirectClientExecutor: ... 161 | -------------------------------------------------------------------------------- /scripts/yandex_direct_export_to_file.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import csv 3 | import json 4 | import logging 5 | from pathlib import Path 6 | from typing import Iterable, Optional 7 | 8 | from tapi_yandex_direct import YandexDirect, exceptions 9 | 10 | LOGGING_FORMAT = "%(asctime)s [%(levelname)s] %(pathname)s:%(funcName)s %(message)s" 11 | 12 | logging.basicConfig(format=LOGGING_FORMAT, level=logging.DEBUG) 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | def create_report_name(body: dict, headers: dict): 17 | return hash(str((body, headers))) 18 | 19 | 20 | def prepare_body(body: dict, headers: dict): 21 | body["params"]["ReportName"] = create_report_name(body, headers) 22 | 23 | 24 | def add_extra_data(row: dict, extra_columns: Iterable, login: Optional[str]): 25 | if "login" in extra_columns: 26 | row["login"] = login 27 | 28 | 29 | def main( 30 | client: YandexDirect, 31 | body: dict, 32 | headers: dict, 33 | resource: str, 34 | extra_columns: Iterable, 35 | login: Optional[str], 36 | filepath: Path, 37 | ) -> None: 38 | response = None 39 | page_iterator = None 40 | api_error_retries = 5 41 | while True: 42 | try: 43 | if response is None: 44 | resource_method = getattr(client, resource) 45 | logger.info(f"Request resource '{resource}'") 46 | response = resource_method().post(data=body, headers=headers) 47 | 48 | if resource != "reports": 49 | 50 | if page_iterator is None: 51 | page_iterator = response().pages() 52 | 53 | page = next(page_iterator) 54 | 55 | except exceptions.YandexDirectClientError as exc: 56 | error_code = int(exc.error_code) 57 | if error_code == 9000: 58 | continue 59 | elif error_code in (52, 1000, 1001, 1002): 60 | if api_error_retries: 61 | api_error_retries -= 1 62 | continue 63 | raise 64 | 65 | except (ConnectionError, TimeoutError): 66 | if api_error_retries: 67 | api_error_retries -= 1 68 | continue 69 | raise 70 | 71 | except StopIteration: 72 | break 73 | 74 | else: 75 | if resource == "reports": 76 | if response.status_code in (201, 202): 77 | response = None 78 | continue 79 | 80 | logger.info(f"Save data to {filepath}") 81 | 82 | if resource == "reports": 83 | if extra_columns: 84 | with open(filepath, "w", newline='') as csvfile: 85 | data_iterator = response().iter_dicts() 86 | 87 | for i, row in enumerate(data_iterator): 88 | if i == 0: 89 | writer = csv.DictWriter( 90 | csvfile, fieldnames=row.keys(), dialect="excel-tab", 91 | ) 92 | writer.writeheader() 93 | 94 | add_extra_data(row, extra_columns, login) 95 | writer.writerow(row) 96 | else: 97 | with open(filepath, "w") as csvfile: 98 | csvfile.write(response.data) 99 | 100 | # The report has no pagination. 101 | break 102 | else: 103 | with open(filepath, "w", newline='') as csvfile: 104 | data_iterator = page().items() 105 | 106 | for i, row in enumerate(data_iterator): 107 | if i == 0: 108 | writer = csv.DictWriter( 109 | csvfile, fieldnames=row.keys(), dialect="excel-tab" 110 | ) 111 | writer.writeheader() 112 | 113 | add_extra_data(row, extra_columns, login) 114 | writer.writerow(row) 115 | 116 | 117 | if __name__ == "__main__": 118 | parser = argparse.ArgumentParser( 119 | description="Export data from Yandex Direct to tsv file" 120 | ) 121 | parser.add_argument( 122 | "--token", 123 | required=True, 124 | type=str, 125 | help="Access token, detail https://yandex.ru/dev/direct/doc/dg/concepts/auth-token.html", 126 | ) 127 | parser.add_argument( 128 | "--login", 129 | required=False, 130 | type=str, 131 | help="If you are making inquiries from an agency account, you must be sure to specify the account login" 132 | ) 133 | parser.add_argument( 134 | "--extra_columns", 135 | nargs='+', 136 | required=False, 137 | type=str, 138 | choices=["login"], 139 | default=[], 140 | help="", 141 | ) 142 | parser.add_argument( 143 | "--body_filepath", 144 | required=True, 145 | type=str, 146 | help="Request body, detail https://yandex.ru/dev/direct/doc/dg/concepts/JSON.html#JSON__json-request " 147 | "and https://yandex.ru/dev/direct/doc/reports/spec.html", 148 | ) 149 | parser.add_argument( 150 | "--filepath", 151 | required=True, 152 | type=str, 153 | help="File path for save data", 154 | ) 155 | parser.add_argument( 156 | "--use_operator_units", 157 | required=False, 158 | type=bool, 159 | help="HTTP-header, detail https://yandex.ru/dev/direct/doc/reports/headers.html", 160 | default=False, 161 | ) 162 | parser.add_argument( 163 | "--return_money_in_micros", 164 | required=False, 165 | type=bool, 166 | help="HTTP-header, detail https://yandex.ru/dev/direct/doc/reports/headers.html", 167 | default=False, 168 | ) 169 | parser.add_argument( 170 | "--language", 171 | required=False, 172 | type=str, 173 | default="ru", 174 | help="The language in which the data for directories and errors will be returned", 175 | ) 176 | 177 | parser.add_argument( 178 | "--resource", 179 | required=True, 180 | type=str, 181 | help="", 182 | choices=[ 183 | "reports", 184 | "adextensions", 185 | "adgroups", 186 | "adimages", 187 | "ads", 188 | "agencyclients", 189 | "audiencetargets", 190 | "bidmodifiers", 191 | "bids", 192 | "businesses", 193 | "campaigns", 194 | "changes", 195 | "clients", 196 | "creatives", 197 | "dictionaries", 198 | "dynamicads", 199 | "feeds", 200 | "keywordbids", 201 | "keywords", 202 | "keywordsresearch", 203 | "leads", 204 | "negativekeywordsharedsets", 205 | "retargeting", 206 | "sitelinks", 207 | "smartadtargets", 208 | "turbopages", 209 | "vcards", 210 | ], 211 | ) 212 | args = parser.parse_args() 213 | 214 | client = YandexDirect( 215 | access_token=args.token, 216 | login=args.login, 217 | retry_if_not_enough_units=True, 218 | language=args.language, 219 | retry_if_exceeded_limit=True, 220 | retries_if_server_error=5, 221 | wait_report=True, 222 | ) 223 | headers = { 224 | "use_operator_units": str(args.use_operator_units), 225 | "return_money_in_micros": str(args.return_money_in_micros), 226 | } 227 | 228 | with open(args.body_filepath) as f: 229 | body_text = f.read() 230 | 231 | body = json.loads(body_text) 232 | if args.resource == "reports": 233 | prepare_body(body, headers) 234 | 235 | main( 236 | client, 237 | body, 238 | headers, 239 | args.resource, 240 | args.extra_columns, 241 | args.login, 242 | args.filepath, 243 | ) 244 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python client for [API Yandex Direct](https://yandex.ru/dev/metrika/doc/api2/concept/about-docpage/) 2 | 3 | ![Supported Python Versions](https://img.shields.io/static/v1?label=python&message=>=3.6&color=green) 4 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/vintasoftware/tapioca-wrapper/master/LICENSE) 5 | [![Downloads](https://pepy.tech/badge/tapi-yandex-direct)](https://pepy.tech/project/tapi-yandex-direct) 6 | Code style: black 7 | 8 | ## Installation 9 | 10 | Prev version 11 | 12 | pip install --upgrade tapi-yandex-direct==2020.12.15 13 | 14 | Last version. Has backward incompatible changes. 15 | 16 | pip install --upgrade tapi-yandex-direct==2021.5.29 17 | 18 | ## Examples 19 | 20 | [Ipython Notebook](https://github.com/pavelmaksimov/tapi-yandex-direct/blob/master/examples.ipynb) 21 | 22 | [Script to export data to a file](scripts/yandex_direct_export_to_file.py) 23 | 24 | python yandex_direct_export_to_file.py --help 25 | python yandex_direct_export_to_file.py --body_filepath body-clients.json --token TOKEN --resource clients --filepath clients.tsv 26 | python yandex_direct_export_to_file.py --body_filepath body-report.json --token TOKEN --resource reports --filepath report-with-login-column.tsv --extra_columns login 27 | python yandex_direct_export_to_file.py --body_filepath body-report.json --token TOKEN --resource reports --filepath report.tsv 28 | 29 | 30 | ## Documentation 31 | [Справка](https://yandex.ru/dev/direct/) Api Яндекс Директ 32 | 33 | 34 | ### Client params 35 | ```python 36 | from tapi_yandex_direct import YandexDirect 37 | 38 | ACCESS_TOKEN = "{your_access_token}" 39 | 40 | client = YandexDirect( 41 | # Required parameters: 42 | 43 | access_token=ACCESS_TOKEN, 44 | # If you are making inquiries from an agent account, you must be sure to specify the account login. 45 | login="{login}", 46 | 47 | # Optional parameters: 48 | 49 | # Enable sandbox. 50 | is_sandbox=False, 51 | # Repeat request when units run out 52 | retry_if_not_enough_units=False, 53 | # The language in which the data for directories and errors will be returned. 54 | language="ru", 55 | # Repeat the request if the limits on the number of reports or requests are exceeded. 56 | retry_if_exceeded_limit=True, 57 | # Number of retries when server errors occur. 58 | retries_if_server_error=5 59 | ) 60 | ``` 61 | 62 | ### Resource methods 63 | ```python 64 | print(dir(client)) 65 | [ 66 | "adextensions", 67 | "adgroups", 68 | "adimages", 69 | "ads", 70 | "agencyclients", 71 | "audiencetargets", 72 | "bidmodifiers", 73 | "bids", 74 | "businesses", 75 | "campaigns", 76 | "changes", 77 | "clients", 78 | "creatives", 79 | "debugtoken", 80 | "dictionaries", 81 | "dynamicads", 82 | "feeds", 83 | "keywordbids", 84 | "keywords", 85 | "keywordsresearch", 86 | "leads", 87 | "negativekeywordsharedsets", 88 | "reports", 89 | "retargeting", 90 | "sitelinks", 91 | "smartadtargets", 92 | "turbopages", 93 | "vcards", 94 | ] 95 | ``` 96 | or look into [resource mapping](tapi_yandex_direct/resource_mapping.py) 97 | 98 | ### Request 99 | 100 | API requests are made over HTTPS using the POST method. 101 | Input data structures are passed in the body of the request. 102 | 103 | ```python 104 | import datetime as dt 105 | 106 | # Get campaigns. 107 | body = { 108 | "method": "get", 109 | "params": { 110 | "SelectionCriteria": {}, 111 | "FieldNames": ["Id","Name"], 112 | }, 113 | } 114 | campaigns = client.campaigns().post(data=body) 115 | print(campaigns) 116 | # 122 | 123 | 124 | # Extract raw data. 125 | data = campaigns.data 126 | assert isinstance(data, dict) 127 | 128 | 129 | # Create a campaign. 130 | body = { 131 | "method": "add", 132 | "params": { 133 | "Campaigns": [ 134 | { 135 | "Name": "MyCampaignTest", 136 | "StartDate": str(dt.datetime.now().date()), 137 | "TextCampaign": { 138 | "BiddingStrategy": { 139 | "Search": { 140 | "BiddingStrategyType": "HIGHEST_POSITION" 141 | }, 142 | "Network": { 143 | "BiddingStrategyType": "SERVING_OFF" 144 | } 145 | }, 146 | "Settings": [] 147 | } 148 | } 149 | ] 150 | } 151 | } 152 | result = client.campaigns().post(data=body) 153 | print(result) 154 | # 156 | 157 | # Extract raw data. 158 | data = campaigns.data 159 | assert isinstance(data, dict) 160 | print(result) 161 | # {'result': {'AddResults': [{'Id': 417066}]}} 162 | ``` 163 | 164 | 165 | ### Client methods 166 | 167 | Result extraction method. 168 | 169 | ```python 170 | body = { 171 | "method": "get", 172 | "params": { 173 | "SelectionCriteria": {}, 174 | "FieldNames": ["Id","Name"], 175 | }, 176 | } 177 | campaigns = client.campaigns().post(data=body) 178 | 179 | # Request response. 180 | print(campaigns.response) 181 | print(campaigns.request_kwargs) 182 | print(campaigns.status_code) 183 | print(campaigns.data) 184 | ``` 185 | 186 | ### .extract() 187 | 188 | Result extraction method. 189 | 190 | ```python 191 | body = { 192 | "method": "get", 193 | "params": { 194 | "SelectionCriteria": {}, 195 | "FieldNames": ["Id","Name"], 196 | }, 197 | } 198 | campaigns = client.campaigns().post(data=body) 199 | campaigns_list = campaigns().extract() 200 | assert isinstance(campaigns_list, list) 201 | print(campaigns_list) 202 | # [{'Id': 338157, 'Name': 'Test API Sandbox campaign 1'}, 203 | # {'Id': 338158, 'Name': 'Test API Sandbox campaign 2'}] 204 | ``` 205 | 206 | 207 | ### .items() 208 | 209 | Iterating result items. 210 | 211 | ```python 212 | body = { 213 | "method": "get", 214 | "params": { 215 | "SelectionCriteria": {}, 216 | "FieldNames": ["Id","Name"], 217 | }, 218 | } 219 | campaigns = client.campaigns().post(data=body) 220 | 221 | for item in campaigns().items(): 222 | print(item) 223 | # {'Id': 338157, 'Name': 'Test API Sandbox campaign 1'} 224 | assert isinstance(item, dict) 225 | ``` 226 | 227 | 228 | ### .pages() 229 | 230 | Iterating to get all the data. 231 | 232 | ```python 233 | body = { 234 | "method": "get", 235 | "params": { 236 | "SelectionCriteria": {}, 237 | "FieldNames": ["Id","Name"], 238 | "Page": {"Limit": 2} 239 | }, 240 | } 241 | campaigns = client.campaigns().post(data=body) 242 | 243 | # Iterating requests data. 244 | for page in campaigns().pages(): 245 | assert isinstance(page.data, list) 246 | print(page.data) 247 | # [{'Id': 338157, 'Name': 'Test API Sandbox campaign 1'}, 248 | # {'Name': 'Test API Sandbox campaign 2', 'Id': 338158}] 249 | 250 | # Iterating items of page data. 251 | for item in page().items(): 252 | print(item) 253 | # {'Id': 338157, 'Name': 'Test API Sandbox campaign 1'} 254 | assert isinstance(item, dict) 255 | ``` 256 | 257 | 258 | ### .iter_items() 259 | 260 | After each request, iterates over the items of the request data. 261 | 262 | ```python 263 | body = { 264 | "method": "get", 265 | "params": { 266 | "SelectionCriteria": {}, 267 | "FieldNames": ["Id","Name"], 268 | "Page": {"Limit": 2} 269 | }, 270 | } 271 | campaigns = client.campaigns().post(data=body) 272 | 273 | # Iterates through the elements of all data. 274 | for item in campaigns().iter_items(): 275 | assert isinstance(item, dict) 276 | print(item) 277 | 278 | # {'Name': 'MyCampaignTest', 'Id': 417065} 279 | # {'Name': 'MyCampaignTest', 'Id': 417066} 280 | # {'Id': 338157, 'Name': 'Test API Sandbox campaign 1'} 281 | # {'Name': 'Test API Sandbox campaign 2', 'Id': 338158} 282 | # {'Id': 338159, 'Name': 'Test API Sandbox campaign 3'} 283 | # {'Name': 'MyCampaignTest', 'Id': 415805} 284 | # {'Id': 416524, 'Name': 'MyCampaignTest'} 285 | # {'Id': 417056, 'Name': 'MyCampaignTest'} 286 | # {'Id': 417057, 'Name': 'MyCampaignTest'} 287 | # {'Id': 417058, 'Name': 'MyCampaignTest'} 288 | # {'Id': 417065, 'Name': 'MyCampaignTest'} 289 | # {'Name': 'MyCampaignTest', 'Id': 417066} 290 | ``` 291 | 292 | 293 | ## Reports 294 | 295 | ```python 296 | from tapi_yandex_direct import YandexDirect 297 | 298 | ACCESS_TOKEN = "{ваш токен доступа}" 299 | 300 | client = YandexDirect( 301 | # Required parameters: 302 | 303 | access_token=ACCESS_TOKEN, 304 | # If you are making inquiries from an agent account, you must be sure to specify the account login. 305 | login="{login}", 306 | 307 | # Optional parameters: 308 | 309 | # Enable sandbox. 310 | is_sandbox=False, 311 | # Repeat request when units run out 312 | retry_if_not_enough_units=False, 313 | # The language in which the data for directories and errors will be returned. 314 | language="ru", 315 | # Repeat the request if the limits on the number of reports or requests are exceeded. 316 | retry_if_exceeded_limit=True, 317 | # Number of retries when server errors occur. 318 | retries_if_server_error=5, 319 | 320 | # Report resource parameters: 321 | 322 | # Report generation mode: online, offline or auto. 323 | processing_mode="offline", 324 | # When requesting a report, it will wait until the report is prepared and download it. 325 | wait_report=True, 326 | # Monetary values in the report are returned in currency with an accuracy of two decimal places. 327 | return_money_in_micros=False, 328 | # Do not display a line with the report name and date range in the report. 329 | skip_report_header=True, 330 | # Do not display a line with field names in the report. 331 | skip_column_header=False, 332 | # Do not display a line with the number of statistics lines in the report. 333 | skip_report_summary=True, 334 | ) 335 | 336 | body = { 337 | "params": { 338 | "SelectionCriteria": {}, 339 | "FieldNames": ["Date", "CampaignId", "Clicks", "Cost"], 340 | "OrderBy": [{ 341 | "Field": "Date" 342 | }], 343 | "ReportName": "Actual Data", 344 | "ReportType": "CAMPAIGN_PERFORMANCE_REPORT", 345 | "DateRangeType": "LAST_WEEK", 346 | "Format": "TSV", 347 | "IncludeVAT": "YES", 348 | "IncludeDiscount": "YES" 349 | } 350 | } 351 | report = client.reports().post(data=body) 352 | print(report.data) 353 | # 'Date\tCampaignId\tClicks\tCost\n' 354 | # '2019-09-02\t338151\t12578\t9210750000\n' 355 | ``` 356 | 357 | 358 | ### .columns 359 | 360 | Extract column names. 361 | ```python 362 | report = client.reports().post(data=body) 363 | print(report.columns) 364 | # ['Date', 'CampaignId', 'Clicks', 'Cost'] 365 | ``` 366 | 367 | 368 | ### .to_lines() 369 | 370 | ```python 371 | report = client.reports().post(data=body) 372 | print(report().to_lines()) 373 | # list[str] 374 | # [..., '2019-09-02\t338151\t12578\t9210750000'] 375 | ``` 376 | 377 | 378 | ### .to_values() 379 | 380 | ```python 381 | report = client.reports().post(data=body) 382 | print(report().to_values()) 383 | # list[list[str]] 384 | # [..., ['2019-09-02', '338151', '12578', '9210750000']] 385 | ``` 386 | 387 | 388 | ### .to_dicts() 389 | 390 | ```python 391 | report = client.reports().post(data=body) 392 | print(report().to_dicts()) 393 | # list[dict] 394 | # [..., {'Date': '2019-09-02', 'CampaignId': '338151', 'Clicks': '12578', 'Cost': 9210750000'}] 395 | ``` 396 | 397 | 398 | ### .to_columns() 399 | 400 | ```python 401 | report = client.reports().post(data=body) 402 | print(report().to_columns()) 403 | # list[list[str], list[str], list[str], list[str]] 404 | # [[..., '2019-09-02'], [..., '338151'], [..., '12578'], [..., '9210750000']] 405 | ``` 406 | 407 | 408 | ## Features 409 | 410 | Information about the resource. 411 | ```python 412 | client.campaigns().help() 413 | ``` 414 | 415 | Open resource documentation 416 | ```python 417 | client.campaigns().open_docs() 418 | ``` 419 | 420 | Send a request in the browser. 421 | ```python 422 | client.campaigns().open_in_browser() 423 | ``` 424 | 425 | 426 | ## Dependences 427 | - requests 428 | - [tapi_wrapper](https://github.com/pavelmaksimov/tapi-wrapper) 429 | 430 | 431 | ## CHANGELOG 432 | v2021.5.29 433 | - Fix stub file (syntax highlighting) 434 | 435 | 436 | v2021.5.25 437 | - Add stub file (syntax highlighting) 438 | - Add methods 'iter_dicts', 'to_dicts' 439 | 440 | 441 | ## Автор 442 | Павел Максимов 443 | 444 | Связаться со мной можно в 445 | [Телеграм](https://t.me/pavel_maksimow) 446 | и в 447 | [Facebook](https://www.facebook.com/pavel.maksimow) 448 | 449 | Удачи тебе, друг! Поставь звездочку ;) 450 | -------------------------------------------------------------------------------- /tapi_yandex_direct/tapi_yandex_direct.py: -------------------------------------------------------------------------------- 1 | import io 2 | import logging 3 | import time 4 | from typing import Union, Optional, Dict, List, Iterator 5 | 6 | import orjson 7 | from requests import Response 8 | from tapi2 import TapiAdapter, generate_wrapper_from_adapter, JSONAdapterMixin 9 | from tapi2.exceptions import ResponseProcessException, ClientError, TapiException 10 | 11 | from tapi_yandex_direct import exceptions 12 | from tapi_yandex_direct.resource_mapping import RESOURCE_MAPPING_V5 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | RESULT_DICTIONARY_KEYS_OF_API_METHODS = { 17 | "add": "AddResults", 18 | "update": "UpdateResults", 19 | "unarchive": "UnarchiveResults", 20 | "suspend": "SuspendResults", 21 | "resume": "ResumeResults", 22 | "delete": "DeleteResults", 23 | "archive": "ArchiveResults", 24 | "moderate": "ModerateResults", 25 | "setBids": "SetBidsResults", 26 | "set": "SetResults", 27 | "setAuto": "SetAutoResults", 28 | "toggle": "ToggleResults", 29 | "checkDictionaries": "result", 30 | "checkCampaigns": "Campaigns", 31 | "check": "Modified", 32 | "HasSearchVolumeResults": "HasSearchVolumeResults", 33 | "get": { 34 | "/json/v5/campaigns": "Campaigns", 35 | "/json/v5/adgroups": "AdGroups", 36 | "/json/v5/ads": "Ads", 37 | "/json/v5/audiencetargets": "AudienceTargets", 38 | "/json/v5/creatives": "Creatives", 39 | "/json/v5/adimages": "AdImages", 40 | "/json/v5/vcards": "VCards", 41 | "/json/v5/sitelinks": "SitelinksSets", 42 | "/json/v5/adextensions": "AdExtensions", 43 | "/json/v5/keywords": "Keywords", 44 | "/json/v5/retargetinglists": "RetargetingLists", 45 | "/json/v5/bids": "Bids", 46 | "/json/v5/keywordbids": "KeywordBids", 47 | "/json/v5/bidmodifiers": "BidModifiers", 48 | "/json/v5/agencyclients": "Clients", 49 | "/json/v5/clients": "Clients", 50 | "/json/v5/leads": "Leads", 51 | "/json/v5/dynamictextadtargets": "Webpages", 52 | "/json/v5/turbopages": "TurboPages", 53 | "/json/v5/negativekeywordsharedsets": "NegativeKeywordSharedSets", 54 | "/json/v5/feeds": "Feeds", 55 | "/json/v5/smartadtargets": "SmartAdTargets", 56 | "/json/v5/businesses": "Businesses", 57 | }, 58 | } 59 | REPORTS_RESOURCE_URL = "/json/v5/reports" 60 | 61 | 62 | class YandexDirectClientAdapter(JSONAdapterMixin, TapiAdapter): 63 | resource_mapping = RESOURCE_MAPPING_V5 64 | 65 | def __init__(self, *args, **kwargs): 66 | super().__init__(*args, **kwargs) 67 | 68 | def get_api_root(self, api_params: dict, resource_name: str) -> str: 69 | if resource_name == "debugtoken": 70 | return "https://" 71 | elif api_params.get("is_sandbox"): 72 | return "https://api-sandbox.direct.yandex.com/" 73 | else: 74 | return "https://api.direct.yandex.com/" 75 | 76 | def get_request_kwargs(self, api_params: dict, *args, **kwargs) -> dict: 77 | """Обогащение запроса, параметрами""" 78 | params = super().get_request_kwargs(api_params, *args, **kwargs) 79 | 80 | token = api_params.get("access_token") 81 | if token: 82 | params["headers"].update({"Authorization": "Bearer {}".format(token)}) 83 | 84 | login = api_params.get("login") 85 | if login: 86 | params["headers"].update({"Client-Login": login}) 87 | 88 | use_operator_units = api_params.get("use_operator_units") 89 | if use_operator_units: 90 | params["headers"].update({"Use-Operator-Units": use_operator_units}) 91 | 92 | language = api_params.get("language") 93 | if language: 94 | params["headers"].update({"Accept-Language": language}) 95 | 96 | params["headers"]["processingMode"] = api_params.get("processing_mode", "auto") 97 | params["headers"]["returnMoneyInMicros"] = str( 98 | api_params.get("return_money_in_micros", False) 99 | ).lower() 100 | params["headers"]["skipReportHeader"] = str( 101 | api_params.get("skip_report_header", True) 102 | ).lower() 103 | params["headers"]["skipColumnHeader"] = str( 104 | api_params.get("skip_column_header", False) 105 | ).lower() 106 | params["headers"]["skipReportSummary"] = str( 107 | api_params.get("skip_report_summary", True) 108 | ).lower() 109 | 110 | if "receive_all_objects" in api_params: 111 | raise exceptions.BackwardCompatibilityError( 112 | "parameter 'receive_all_objects'" 113 | ) 114 | 115 | if "auto_request_generation" in api_params: 116 | raise exceptions.BackwardCompatibilityError( 117 | "parameter 'auto_request_generation'" 118 | ) 119 | 120 | return params 121 | 122 | def get_error_message( 123 | self, data: Union[None, dict], response: Response = None 124 | ) -> dict: 125 | if data is None: 126 | return {"error_text": response.content.decode()} 127 | else: 128 | return data 129 | 130 | def format_data_to_request(self, data) -> Optional[bytes]: 131 | if data: 132 | return orjson.dumps(data) 133 | 134 | def response_to_native(self, response: Response) -> Union[dict, str]: 135 | if response.content.strip(): 136 | try: 137 | return orjson.loads(response.content.decode()) 138 | except ValueError: 139 | return response.text 140 | 141 | def process_response( 142 | self, response: Response, request_kwargs: dict, **kwargs 143 | ) -> dict: 144 | request_kwargs["data"] = orjson.loads(request_kwargs["data"]) 145 | 146 | if response.status_code == 502: 147 | raise exceptions.YandexDirectApiError( 148 | response, 149 | "The report generation time has exceeded the server limit. " 150 | "Please try to change the request parameters, " 151 | "reduce the period or the amount of requested data.", 152 | **kwargs 153 | ) 154 | elif response.status_code == 405: 155 | raise exceptions.YandexDirectApiError( 156 | response, 157 | "This resource does not support the HTTP method {}\n".format( 158 | response.request.method 159 | ), 160 | **kwargs 161 | ) 162 | 163 | data = self.response_to_native(response) 164 | 165 | if isinstance(data, dict) and data.get("error"): 166 | raise ResponseProcessException(ClientError, data) 167 | elif response.status_code in (201, 202): 168 | raise ResponseProcessException(ClientError, data) 169 | else: 170 | data = super().process_response(response, request_kwargs, **kwargs) 171 | 172 | if response.request.path_url == REPORTS_RESOURCE_URL: 173 | lines = self._iter_lines(data=data, response=response, **kwargs) 174 | kwargs["store"]["columns"] = next(lines).split("\t") 175 | else: 176 | kwargs["store"].pop("columns", None) 177 | 178 | return data 179 | 180 | def error_handling( 181 | self, 182 | tapi_exception: TapiException, 183 | error_message: dict, 184 | repeat_number: int, 185 | response: Response, 186 | request_kwargs: dict, 187 | api_params: dict, 188 | **kwargs 189 | ) -> None: 190 | if response.status_code in (201, 202): 191 | pass 192 | elif "error_text" in error_message: 193 | raise exceptions.YandexDirectApiError( 194 | response, error_message["error_text"], **kwargs 195 | ) 196 | else: 197 | error_data = error_message.get("error", {}) 198 | error_code = int(error_data.get("code", 0)) 199 | 200 | if error_code == 152: 201 | raise exceptions.YandexDirectNotEnoughUnitsError( 202 | response, error_message, **kwargs 203 | ) 204 | elif ( 205 | error_code == 53 206 | or error_data["error_detail"] == "OAuth token is missing" 207 | ): 208 | raise exceptions.YandexDirectTokenError( 209 | response, error_message, **kwargs 210 | ) 211 | elif error_code in (56, 506, 9000): 212 | raise exceptions.YandexDirectRequestsLimitError( 213 | response, error_message, **kwargs 214 | ) 215 | else: 216 | raise exceptions.YandexDirectClientError( 217 | response, error_message, **kwargs 218 | ) 219 | 220 | def retry_request( 221 | self, 222 | tapi_exception: TapiException, 223 | error_message: dict, 224 | repeat_number: int, 225 | response: Response, 226 | request_kwargs: dict, 227 | api_params: dict, 228 | **kwargs 229 | ) -> bool: 230 | status_code = response.status_code 231 | error_data = error_message.get("error", {}) 232 | error_code = int(error_data.get("code", 0)) 233 | 234 | if status_code in (201, 202): 235 | logger.info("Report not ready") 236 | if api_params.get("wait_report", True): 237 | sleep = int(response.headers.get("retryIn", 10)) 238 | logger.info("Re-request after {} seconds".format(sleep)) 239 | time.sleep(sleep) 240 | return True 241 | 242 | if error_code == 152: 243 | if api_params.get("retry_if_not_enough_units", False): 244 | logger.warning("Not enough units, re-request after 5 minutes") 245 | time.sleep(60 * 5) 246 | return True 247 | else: 248 | logger.error("Not enough units to request") 249 | 250 | elif error_code == 506 and api_params.get("retry_if_exceeded_limit", True): 251 | logger.warning("API requests exceeded, re-request after 10 seconds") 252 | time.sleep(10) 253 | return True 254 | 255 | elif error_code == 56 and api_params.get("retry_if_exceeded_limit", True): 256 | logger.warning("Method request limit exceeded. Re-request after 10 seconds") 257 | time.sleep(10) 258 | return True 259 | 260 | elif error_code == 9000 and api_params.get("retry_if_exceeded_limit", True): 261 | logger.warning( 262 | "Created by max number of reports. Re-request after 10 seconds" 263 | ) 264 | time.sleep(10) 265 | return True 266 | 267 | elif error_code in (52, 1000, 1001, 1002) or status_code == 500: 268 | if repeat_number < api_params.get("retries_if_server_error", 5): 269 | logger.warning("Server error. Re-request after 1 second") 270 | time.sleep(1) 271 | return True 272 | 273 | return False 274 | 275 | def get_iterator_next_request_kwargs( 276 | self, 277 | response_data: Dict[str, dict], 278 | response: Response, 279 | request_kwargs: dict, 280 | api_params: dict, 281 | **kwargs 282 | ) -> Optional[dict]: 283 | limit = response_data["result"].get("LimitedBy") 284 | if limit: 285 | page = request_kwargs["data"]["params"].setdefault("Page", {}) 286 | page["Offset"] = limit 287 | 288 | return request_kwargs 289 | 290 | def get_iterator_pages(self, response_data: dict, **kwargs) -> List[List[dict]]: 291 | return [self.extract(response_data, **kwargs)] 292 | 293 | def get_iterator_items(self, data: Union[dict, List[dict]], **kwargs) -> List[dict]: 294 | if "result" in data: 295 | return self.extract(data, **kwargs) 296 | return data 297 | 298 | def get_iterator_iteritems(self, response_data: dict, **kwargs) -> List[dict]: 299 | return self.extract(response_data, **kwargs) 300 | 301 | def _iter_lines(self, data: str, response: Response, **kwargs) -> Iterator[str]: 302 | if response.request.path_url != REPORTS_RESOURCE_URL: 303 | raise NotImplementedError("For reports resource only") 304 | 305 | lines = io.StringIO(data) 306 | iterator = (line.replace("\n", "") for line in lines) 307 | 308 | return iterator 309 | 310 | def iter_lines(self, **kwargs) -> Iterator[str]: 311 | iterator = self._iter_lines(**kwargs) 312 | next(iterator) # skipping columns 313 | yield from iterator 314 | 315 | def iter_values(self, **kwargs) -> Iterator[list]: 316 | for line in self.iter_lines(**kwargs): 317 | yield line.split("\t") 318 | 319 | def iter_dicts(self, **kwargs) -> Iterator[dict]: 320 | for line in self.iter_lines(**kwargs): 321 | yield dict(zip(kwargs["store"]["columns"], line.split("\t"))) 322 | 323 | def to_values(self, **kwargs) -> List[list]: 324 | return list(self.iter_values(**kwargs)) 325 | 326 | def to_lines(self, **kwargs) -> List[str]: 327 | return list(self.iter_lines(**kwargs)) 328 | 329 | def to_columns(self, **kwargs): 330 | columns = [[] for _ in range(len(kwargs["store"]["columns"]))] 331 | for values in self.iter_values(**kwargs): 332 | for i, col in enumerate(columns): 333 | col.append(values[i]) 334 | 335 | return columns 336 | 337 | def to_dict(self, **kwargs) -> List[dict]: 338 | return [ 339 | dict(zip(kwargs["store"]["columns"], values)) 340 | for values in self.iter_values(**kwargs) 341 | ] 342 | 343 | def to_dicts(self, **kwargs) -> List[dict]: 344 | return self.to_dict(**kwargs) 345 | 346 | def extract( 347 | self, data: dict, response: Response, request_kwargs: dict, **kwargs 348 | ) -> List[dict]: 349 | if response.request.path_url == REPORTS_RESOURCE_URL: 350 | raise NotImplementedError("Report resource not supported") 351 | 352 | method = request_kwargs["data"]["method"] 353 | try: 354 | key = RESULT_DICTIONARY_KEYS_OF_API_METHODS[method] 355 | except KeyError: 356 | raise KeyError( 357 | "Result extract is not implemented for method '{}'".format(method) 358 | ) 359 | else: 360 | if method == "get": 361 | resource_map = key 362 | try: 363 | key = resource_map[response.request.path_url] 364 | except KeyError: 365 | raise KeyError( 366 | "Result extract is not implemented for resource '{}'".format( 367 | response.request.path_url 368 | ) 369 | ) 370 | else: 371 | return data.get("result", {}).get(key, []) 372 | else: 373 | data = data["result"] 374 | if key == "result": 375 | return data 376 | return data[key] 377 | 378 | def transform(self, *args, **kwargs): 379 | raise exceptions.BackwardCompatibilityError("method 'transform'") 380 | 381 | 382 | YandexDirect = generate_wrapper_from_adapter(YandexDirectClientAdapter) 383 | -------------------------------------------------------------------------------- /examples.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "outputs": [], 7 | "source": ["!pip install tapi-yandex-direct"], 8 | "metadata": {"collapsed": false, "pycharm": {"name": "#%%\n"}}, 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 2, 13 | "metadata": {"collapsed": true}, 14 | "outputs": [], 15 | "source": [ 16 | "import datetime as dt\n", 17 | "from pprint import pprint\n", 18 | "from tapi_yandex_direct import YandexDirect", 19 | ], 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 3, 24 | "metadata": {"collapsed": true}, 25 | "outputs": [], 26 | "source": ['ACCESS_TOKEN = ""'], 27 | }, 28 | { 29 | "cell_type": "code", 30 | "execution_count": 4, 31 | "metadata": {"collapsed": true}, 32 | "outputs": [], 33 | "source": [ 34 | "client = YandexDirect(\n", 35 | " # Токен доступа.\n", 36 | " access_token=ACCESS_TOKEN,\n", 37 | " # Не будет повторять запрос, если закончаться баллы.\n", 38 | " retry_if_not_enough_units=False,\n", 39 | ")", 40 | ], 41 | }, 42 | { 43 | "cell_type": "markdown", 44 | "metadata": {}, 45 | "source": ["### Доступные ресурсы API Я.Директ"], 46 | }, 47 | { 48 | "cell_type": "code", 49 | "execution_count": 5, 50 | "metadata": {}, 51 | "outputs": [ 52 | { 53 | "name": "stdout", 54 | "output_type": "stream", 55 | "text": [ 56 | "['adextensions',\n", 57 | " 'adgroups',\n", 58 | " 'adimages',\n", 59 | " 'ads',\n", 60 | " 'agencyclients',\n", 61 | " 'audiencetargets',\n", 62 | " 'bidmodifiers',\n", 63 | " 'bids',\n", 64 | " 'businesses',\n", 65 | " 'campaigns',\n", 66 | " 'changes',\n", 67 | " 'clients',\n", 68 | " 'creatives',\n", 69 | " 'debugtoken',\n", 70 | " 'dictionaries',\n", 71 | " 'dynamicads',\n", 72 | " 'feeds',\n", 73 | " 'keywordbids',\n", 74 | " 'keywords',\n", 75 | " 'keywordsresearch',\n", 76 | " 'leads',\n", 77 | " 'negativekeywordsharedsets',\n", 78 | " 'reports',\n", 79 | " 'retargeting',\n", 80 | " 'sitelinks',\n", 81 | " 'smartadtargets',\n", 82 | " 'turbopages',\n", 83 | " 'vcards']\n", 84 | ], 85 | } 86 | ], 87 | "source": ["pprint(dir(client))"], 88 | }, 89 | {"cell_type": "markdown", "metadata": {}, "source": ["### Создание кампании"]}, 90 | { 91 | "cell_type": "code", 92 | "execution_count": 6, 93 | "metadata": {}, 94 | "outputs": [ 95 | { 96 | "data": { 97 | "text/plain": "{'result': {'AddResults': [{'Id': 61683485}]}}" 98 | }, 99 | "execution_count": 6, 100 | "metadata": {}, 101 | "output_type": "execute_result", 102 | } 103 | ], 104 | "source": [ 105 | "body = {\n", 106 | ' "method": "add",\n', 107 | ' "params": {\n', 108 | ' "Campaigns": [\n', 109 | " {\n", 110 | ' "Name": "MyCampaignTest3",\n', 111 | ' "StartDate": str(dt.datetime.now().date()),\n', 112 | ' "TextCampaign": {\n', 113 | ' "BiddingStrategy": {\n', 114 | ' "Search": {\n', 115 | ' "BiddingStrategyType": "HIGHEST_POSITION"\n', 116 | " },\n", 117 | ' "Network": {\n', 118 | ' "BiddingStrategyType": "SERVING_OFF"\n', 119 | " }\n", 120 | " },\n", 121 | ' "Settings": []\n', 122 | " }\n", 123 | " }\n", 124 | " ]\n", 125 | " }\n", 126 | "}\n", 127 | "campaigns = client.campaigns().post(data=body)\n", 128 | "campaigns.data", 129 | ], 130 | }, 131 | { 132 | "cell_type": "code", 133 | "execution_count": 7, 134 | "metadata": {}, 135 | "outputs": [ 136 | { 137 | "data": {"text/plain": "61683485"}, 138 | "execution_count": 7, 139 | "metadata": {}, 140 | "output_type": "execute_result", 141 | } 142 | ], 143 | "source": [ 144 | "campaigns_list = campaigns().extract()\n", 145 | "campaign_id = campaigns_list[0]['Id']\n", 146 | "campaign_id", 147 | ], 148 | }, 149 | {"cell_type": "markdown", "metadata": {}, "source": ["### Создание группы"]}, 150 | { 151 | "cell_type": "code", 152 | "execution_count": 8, 153 | "metadata": {}, 154 | "outputs": [ 155 | { 156 | "name": "stdout", 157 | "output_type": "stream", 158 | "text": ["{'result': {'AddResults': [{'Id': 4560252809}]}}\n"], 159 | }, 160 | { 161 | "data": {"text/plain": "4560252809"}, 162 | "execution_count": 8, 163 | "metadata": {}, 164 | "output_type": "execute_result", 165 | }, 166 | ], 167 | "source": [ 168 | "body = {\n", 169 | ' "method": "add",\n', 170 | ' "params": {\n', 171 | ' "AdGroups": [\n', 172 | " {\n", 173 | ' "Name": "MyAdGroupTest",\n', 174 | ' "CampaignId": campaign_id,\n', 175 | ' "RegionIds": [0],\n', 176 | " }\n", 177 | " ]\n", 178 | " }\n", 179 | "}\n", 180 | "adgroups = client.adgroups().post(data=body)\n", 181 | "print(adgroups.data)\n", 182 | "adgroups_list = adgroups().extract()\n", 183 | "adgroup_id = adgroups_list[0]['Id']\n", 184 | "adgroup_id", 185 | ], 186 | }, 187 | { 188 | "cell_type": "markdown", 189 | "metadata": {}, 190 | "source": ["### Создание набора быстрых ссылок"], 191 | }, 192 | { 193 | "cell_type": "code", 194 | "execution_count": 9, 195 | "metadata": {}, 196 | "outputs": [ 197 | { 198 | "name": "stdout", 199 | "output_type": "stream", 200 | "text": [ 201 | "{'result': {'AddResults': [{'Id': 1093350622, 'Warnings': [{'Code': 10120, 'Message': 'Specified selection of sitelinks is duplicated in a previously-created selection'}]}]}}\n" 202 | ], 203 | }, 204 | { 205 | "data": {"text/plain": "1093350622"}, 206 | "execution_count": 9, 207 | "metadata": {}, 208 | "output_type": "execute_result", 209 | }, 210 | ], 211 | "source": [ 212 | "body = {\n", 213 | ' "method": "add",\n', 214 | ' "params": {\n', 215 | ' "SitelinksSets": [{\n', 216 | ' "Sitelinks": [\n', 217 | " {\n", 218 | ' "Title": "SitelinkTitle",\n', 219 | ' "Href": "https://yandex.ru",\n', 220 | ' "Description": "SitelinkDescription",\n', 221 | " },{\n", 222 | ' "Title": "SitelinkTitle2",\n', 223 | ' "Href": "https://yandex.ru",\n', 224 | ' "Description": "SitelinkDescription2",\n', 225 | " },\n", 226 | " ]\n", 227 | " }]\n", 228 | " }\n", 229 | "}\n", 230 | "sitelinks = client.sitelinks().post(data=body)\n", 231 | "print(sitelinks.data)\n", 232 | "sitelinks_list = sitelinks().extract()\n", 233 | "sitelinks_id = sitelinks_list[0]['Id']\n", 234 | "sitelinks_id", 235 | ], 236 | }, 237 | { 238 | "cell_type": "markdown", 239 | "metadata": {}, 240 | "source": ["### Создание объявления"], 241 | }, 242 | { 243 | "cell_type": "code", 244 | "execution_count": 10, 245 | "metadata": {}, 246 | "outputs": [ 247 | { 248 | "name": "stdout", 249 | "output_type": "stream", 250 | "text": ["{'result': {'AddResults': [{'Id': 10689207161}]}}\n"], 251 | }, 252 | { 253 | "data": {"text/plain": "10689207161"}, 254 | "execution_count": 10, 255 | "metadata": {}, 256 | "output_type": "execute_result", 257 | }, 258 | ], 259 | "source": [ 260 | "body = {\n", 261 | ' "method": "add",\n', 262 | ' "params": {\n', 263 | ' "Ads": [\n', 264 | " {\n", 265 | ' "AdGroupId": adgroup_id,\n', 266 | ' "TextAd": {\n', 267 | ' "Title": "MyTitleTest",\n', 268 | ' "Text": "MyTextTest",\n', 269 | ' "Mobile": "NO",\n', 270 | ' "Href": "https://yandex.ru",\n', 271 | ' "SitelinkSetId": sitelinks_id\n', 272 | " }\n", 273 | " }\n", 274 | " ]\n", 275 | " }\n", 276 | "}\n", 277 | "ads = client.ads().post(data=body)\n", 278 | "print(ads.data)\n", 279 | "ads_list = ads().extract()\n", 280 | "ad_id = ads_list[0]['Id']\n", 281 | "ad_id", 282 | ], 283 | }, 284 | { 285 | "cell_type": "markdown", 286 | "metadata": {}, 287 | "source": ["### Создание ключевого слова"], 288 | }, 289 | { 290 | "cell_type": "code", 291 | "execution_count": 11, 292 | "metadata": {}, 293 | "outputs": [ 294 | { 295 | "name": "stdout", 296 | "output_type": "stream", 297 | "text": ["{'result': {'AddResults': [{'Id': 31714249487}]}}\n"], 298 | }, 299 | { 300 | "data": {"text/plain": "31714249487"}, 301 | "execution_count": 11, 302 | "metadata": {}, 303 | "output_type": "execute_result", 304 | }, 305 | ], 306 | "source": [ 307 | "body = {\n", 308 | ' "method": "add",\n', 309 | ' "params": {\n', 310 | ' "Keywords": [\n', 311 | " {\n", 312 | ' "AdGroupId": adgroup_id,\n', 313 | ' "Keyword": "Keyword",\n', 314 | ' "Bid": 10 * 1000000,\n', 315 | " }\n", 316 | " ]\n", 317 | " }\n", 318 | "}\n", 319 | "keywords = client.keywords().post(data=body)\n", 320 | "print(keywords.data)\n", 321 | "keywords_list = keywords().extract()\n", 322 | "keyword_id = keywords_list[0]['Id']\n", 323 | "keyword_id", 324 | ], 325 | }, 326 | { 327 | "cell_type": "markdown", 328 | "metadata": {}, 329 | "source": ["### Изменение ставки ключевого слова"], 330 | }, 331 | { 332 | "cell_type": "code", 333 | "execution_count": 12, 334 | "metadata": {}, 335 | "outputs": [ 336 | { 337 | "name": "stdout", 338 | "output_type": "stream", 339 | "text": [ 340 | "{'result': {'SetResults': [{'KeywordId': 31714249487}]}}\n" 341 | ], 342 | }, 343 | { 344 | "data": {"text/plain": "[{'KeywordId': 31714249487}]"}, 345 | "execution_count": 12, 346 | "metadata": {}, 347 | "output_type": "execute_result", 348 | }, 349 | ], 350 | "source": [ 351 | "body = {\n", 352 | ' "method": "set",\n', 353 | ' "params": {\n', 354 | ' "Bids": [{\n', 355 | ' "KeywordId": keyword_id,\n', 356 | ' "Bid": 15 * 1000000\n', 357 | " }]\n", 358 | " }\n", 359 | "}\n", 360 | "bids = client.bids().post(data=body)\n", 361 | "print(bids.data)\n", 362 | "bids().extract()", 363 | ], 364 | }, 365 | { 366 | "cell_type": "markdown", 367 | "metadata": {}, 368 | "source": ["### Добавление минус слов в кампанию"], 369 | }, 370 | { 371 | "cell_type": "code", 372 | "execution_count": 13, 373 | "metadata": {}, 374 | "outputs": [ 375 | { 376 | "name": "stdout", 377 | "output_type": "stream", 378 | "text": ["{'result': {'UpdateResults': [{'Id': 61683485}]}}\n"], 379 | }, 380 | { 381 | "data": {"text/plain": "[{'Id': 61683485}]"}, 382 | "execution_count": 13, 383 | "metadata": {}, 384 | "output_type": "execute_result", 385 | }, 386 | ], 387 | "source": [ 388 | "body = {\n", 389 | ' "method": "update",\n', 390 | ' "params": {\n', 391 | ' "Campaigns": [{\n', 392 | ' "Id": campaign_id,\n', 393 | ' "NegativeKeywords": {"Items": ["минусслово1", "минусслово2"]}\n', 394 | " }]\n", 395 | " }\n", 396 | "}\n", 397 | "result = client.campaigns().post(data=body)\n", 398 | "print(result.data)\n", 399 | "result().extract()", 400 | ], 401 | }, 402 | { 403 | "cell_type": "markdown", 404 | "metadata": {}, 405 | "source": ["#### Проверим, получив данные кампании"], 406 | }, 407 | { 408 | "cell_type": "code", 409 | "execution_count": 14, 410 | "metadata": {}, 411 | "outputs": [ 412 | { 413 | "name": "stdout", 414 | "output_type": "stream", 415 | "text": [ 416 | "{'result': {'Campaigns': [{'NegativeKeywords': {'Items': ['минусслово1', 'минусслово2']}, 'Id': 61683485, 'Name': 'MyCampaignTest3'}]}}\n" 417 | ], 418 | }, 419 | { 420 | "data": { 421 | "text/plain": "[{'Id': 61683485,\n 'Name': 'MyCampaignTest3',\n 'NegativeKeywords': {'Items': ['минусслово1', 'минусслово2']}}]" 422 | }, 423 | "execution_count": 14, 424 | "metadata": {}, 425 | "output_type": "execute_result", 426 | }, 427 | ], 428 | "source": [ 429 | "body = {\n", 430 | ' "method": "get",\n', 431 | ' "params": {\n', 432 | ' "SelectionCriteria": {\n', 433 | ' "Ids": [campaign_id]\n', 434 | " },\n", 435 | ' "FieldNames": [\n', 436 | ' "Id",\n', 437 | ' "Name",\n', 438 | ' "NegativeKeywords"\n', 439 | " ],\n", 440 | " }\n", 441 | "}\n", 442 | "campaigns = client.campaigns().post(data=body)\n", 443 | "print(campaigns.data)\n", 444 | "campaigns().extract()", 445 | ], 446 | }, 447 | { 448 | "cell_type": "markdown", 449 | "metadata": {"collapsed": true}, 450 | "source": ["### Получение всех кампаний"], 451 | }, 452 | { 453 | "cell_type": "code", 454 | "execution_count": 15, 455 | "metadata": {}, 456 | "outputs": [ 457 | { 458 | "name": "stdout", 459 | "output_type": "stream", 460 | "text": [ 461 | "{'Name': 'MyCampaignTest', 'Id': 61682946}\n", 462 | "{'Name': 'MyCampaignTest', 'Id': 61683466}\n", 463 | "{'Name': 'MyCampaignTest2', 'Id': 61683474}\n", 464 | "{'Name': 'MyCampaignTest3', 'Id': 61683476}\n", 465 | "{'Name': 'MyCampaignTest3', 'Id': 61683485}\n", 466 | ], 467 | } 468 | ], 469 | "source": [ 470 | "campaigns = client.campaigns().post(\n", 471 | " data={\n", 472 | ' "method": "get",\n', 473 | ' "params": {\n', 474 | ' "SelectionCriteria": {},\n', 475 | ' "FieldNames": ["Id","Name"],\n', 476 | ' "Page": {"Limit": 1},\n', 477 | " },\n", 478 | " }\n", 479 | ")\n", 480 | "\n", 481 | "campaign_ids = []\n", 482 | "for item in campaigns().iter_items():\n", 483 | " print(item)\n", 484 | ' campaign_ids.append(item["Id"])', 485 | ], 486 | }, 487 | { 488 | "cell_type": "markdown", 489 | "metadata": {"pycharm": {"name": "#%% md\n"}}, 490 | "source": ["Или так"], 491 | }, 492 | { 493 | "cell_type": "code", 494 | "execution_count": 16, 495 | "outputs": [ 496 | { 497 | "name": "stdout", 498 | "output_type": "stream", 499 | "text": [ 500 | "[{'Name': 'MyCampaignTest', 'Id': 61682946}]\n", 501 | "{'Name': 'MyCampaignTest', 'Id': 61682946}\n", 502 | "[{'Name': 'MyCampaignTest', 'Id': 61683466}]\n", 503 | "{'Name': 'MyCampaignTest', 'Id': 61683466}\n", 504 | "[{'Id': 61683474, 'Name': 'MyCampaignTest2'}]\n", 505 | "{'Id': 61683474, 'Name': 'MyCampaignTest2'}\n", 506 | "[{'Name': 'MyCampaignTest3', 'Id': 61683476}]\n", 507 | "{'Name': 'MyCampaignTest3', 'Id': 61683476}\n", 508 | "[{'Id': 61683485, 'Name': 'MyCampaignTest3'}]\n", 509 | "{'Id': 61683485, 'Name': 'MyCampaignTest3'}\n", 510 | ], 511 | } 512 | ], 513 | "source": [ 514 | "campaigns = client.campaigns().post(\n", 515 | " data={\n", 516 | ' "method": "get",\n', 517 | ' "params": {\n', 518 | ' "SelectionCriteria": {},\n', 519 | ' "FieldNames": ["Id","Name"],\n', 520 | ' "Page": {"Limit": 1},\n', 521 | " },\n", 522 | " }\n", 523 | ")\n", 524 | "\n", 525 | "campaign_ids = []\n", 526 | "\n", 527 | "# Будет делать запросы, пока не получит все кампании.\n", 528 | "for page in campaigns().pages():\n", 529 | " print(page.data)\n", 530 | "\n", 531 | " # Есть метод итерирования полученных данных.\n", 532 | " for item in page().items():\n", 533 | " print(item)\n", 534 | ' campaign_ids.append(item["Id"])\n', 535 | ], 536 | "metadata": {"collapsed": false, "pycharm": {"name": "#%%\n"}}, 537 | }, 538 | { 539 | "cell_type": "markdown", 540 | "metadata": {}, 541 | "source": ["### Получение объявлений"], 542 | }, 543 | { 544 | "cell_type": "code", 545 | "execution_count": 17, 546 | "metadata": {}, 547 | "outputs": [ 548 | { 549 | "name": "stdout", 550 | "output_type": "stream", 551 | "text": [ 552 | "{'result': {'Ads': [{'Id': 10689109178, 'Type': 'TEXT_AD', 'TextAd': {'Title': 'MyTitleTest'}}, {'Id': 10689206008, 'Type': 'TEXT_AD', 'TextAd': {'Title': 'MyTitleTest'}}, {'Id': 10689207161, 'Type': 'TEXT_AD', 'TextAd': {'Title': 'MyTitleTest'}}]}}\n" 553 | ], 554 | }, 555 | { 556 | "data": { 557 | "text/plain": "[{'Id': 10689109178, 'TextAd': {'Title': 'MyTitleTest'}, 'Type': 'TEXT_AD'},\n {'Id': 10689206008, 'TextAd': {'Title': 'MyTitleTest'}, 'Type': 'TEXT_AD'},\n {'Id': 10689207161, 'TextAd': {'Title': 'MyTitleTest'}, 'Type': 'TEXT_AD'}]" 558 | }, 559 | "execution_count": 17, 560 | "metadata": {}, 561 | "output_type": "execute_result", 562 | }, 563 | ], 564 | "source": [ 565 | "ads = client.ads().post(\n", 566 | " data={\n", 567 | ' "method": "get",\n', 568 | ' "params": {\n', 569 | ' "SelectionCriteria": {\n', 570 | ' "CampaignIds":campaign_ids,\n', 571 | ' "Types": ["TEXT_AD"]\n', 572 | " },\n", 573 | ' "FieldNames": ["Id", "Type"],\n', 574 | ' "TextAdFieldNames": [\n', 575 | ' "Title"\n', 576 | " ]\n", 577 | " },\n", 578 | " }\n", 579 | ")\n", 580 | "print(ads.data)\n", 581 | "ads().extract()", 582 | ], 583 | }, 584 | { 585 | "cell_type": "markdown", 586 | "metadata": {"collapsed": true}, 587 | "source": ["### Получить идентификаторы объектов, которые были изменены"], 588 | }, 589 | { 590 | "cell_type": "code", 591 | "execution_count": 18, 592 | "metadata": {}, 593 | "outputs": [ 594 | { 595 | "name": "stdout", 596 | "output_type": "stream", 597 | "text": [ 598 | "{'result': {'Modified': {'CampaignIds': [61682946, 61683466, 61683474, 61683476, 61683485], 'AdIds': [10689109178, 10689206008, 10689207161]}, 'Timestamp': '2021-05-07T14:02:28Z'}}\n" 599 | ], 600 | }, 601 | { 602 | "data": { 603 | "text/plain": "{'AdIds': [10689109178, 10689206008, 10689207161],\n 'CampaignIds': [61682946, 61683466, 61683474, 61683476, 61683485]}" 604 | }, 605 | "execution_count": 18, 606 | "metadata": {}, 607 | "output_type": "execute_result", 608 | }, 609 | ], 610 | "source": [ 611 | "body = {\n", 612 | ' "method": "check",\n', 613 | ' "params": {\n', 614 | ' "CampaignIds": campaign_ids,\n', 615 | ' "Timestamp": "2021-01-01T00:00:00Z", # Проверить изменения после указанного времени\n', 616 | ' "FieldNames": ["CampaignIds","AdIds"]\n', 617 | " }\n", 618 | "}\n", 619 | "changes = client.changes().post(data=body)\n", 620 | "print(changes.data)\n", 621 | "changes().extract()", 622 | ], 623 | }, 624 | { 625 | "cell_type": "code", 626 | "execution_count": 19, 627 | "outputs": [ 628 | { 629 | "name": "stdout", 630 | "output_type": "stream", 631 | "text": [ 632 | "{'result': {'Timestamp': '2021-05-07T14:01:20Z', 'Campaigns': [{'ChangesIn': ['SELF', 'CHILDREN'], 'CampaignId': 61682946}, {'CampaignId': 61683466, 'ChangesIn': ['SELF', 'CHILDREN']}, {'ChangesIn': ['SELF'], 'CampaignId': 61683474}, {'ChangesIn': ['SELF'], 'CampaignId': 61683476}, {'CampaignId': 61683485, 'ChangesIn': ['SELF', 'CHILDREN']}]}}\n" 633 | ], 634 | }, 635 | { 636 | "data": { 637 | "text/plain": "[{'CampaignId': 61682946, 'ChangesIn': ['SELF', 'CHILDREN']},\n {'CampaignId': 61683466, 'ChangesIn': ['SELF', 'CHILDREN']},\n {'CampaignId': 61683474, 'ChangesIn': ['SELF']},\n {'CampaignId': 61683476, 'ChangesIn': ['SELF']},\n {'CampaignId': 61683485, 'ChangesIn': ['SELF', 'CHILDREN']}]" 638 | }, 639 | "execution_count": 19, 640 | "metadata": {}, 641 | "output_type": "execute_result", 642 | }, 643 | ], 644 | "source": [ 645 | "body = {\n", 646 | ' "method": "checkCampaigns",\n', 647 | ' "params": {\n', 648 | ' "Timestamp": "2021-01-01T00:00:00Z", # Проверить изменения после указанного времени\n', 649 | " }\n", 650 | "}\n", 651 | "changes = client.changes().post(data=body)\n", 652 | "print(changes.data)\n", 653 | "changes().extract()", 654 | ], 655 | "metadata": {"collapsed": false, "pycharm": {"name": "#%%\n"}}, 656 | }, 657 | ], 658 | "metadata": { 659 | "kernelspec": { 660 | "display_name": "Python 3", 661 | "language": "python", 662 | "name": "python3", 663 | }, 664 | "language_info": { 665 | "codemirror_mode": {"name": "ipython", "version": 3}, 666 | "file_extension": ".py", 667 | "mimetype": "text/x-python", 668 | "name": "python", 669 | "nbconvert_exporter": "python", 670 | "pygments_lexer": "ipython3", 671 | "version": "3.6.3", 672 | }, 673 | }, 674 | "nbformat": 4, 675 | "nbformat_minor": 2, 676 | } 677 | --------------------------------------------------------------------------------