├── 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 | 
4 | [](https://raw.githubusercontent.com/vintasoftware/tapioca-wrapper/master/LICENSE)
5 | [](https://pepy.tech/project/tapi-yandex-direct)
6 |
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 |
--------------------------------------------------------------------------------