yandex_py 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- yandex_py-0.0.1/PKG-INFO +47 -0
- yandex_py-0.0.1/README.md +36 -0
- yandex_py-0.0.1/pyproject.toml +37 -0
- yandex_py-0.0.1/setup.cfg +4 -0
- yandex_py-0.0.1/tests/test_sandbox.py +64 -0
- yandex_py-0.0.1/yandex_py/__init__.py +7 -0
- yandex_py-0.0.1/yandex_py/constants.py +9 -0
- yandex_py-0.0.1/yandex_py/reports/__init__.py +8 -0
- yandex_py-0.0.1/yandex_py/reports/parser.py +35 -0
- yandex_py-0.0.1/yandex_py/reports/report.py +63 -0
- yandex_py-0.0.1/yandex_py/reports/schemas/__init__.py +36 -0
- yandex_py-0.0.1/yandex_py/reports/schemas/fields.py +93 -0
- yandex_py-0.0.1/yandex_py/reports/schemas/headers.py +84 -0
- yandex_py-0.0.1/yandex_py/reports/schemas/request.py +105 -0
- yandex_py-0.0.1/yandex_py/reports/schemas/time_period.py +48 -0
- yandex_py-0.0.1/yandex_py/request_sender/__init__.py +3 -0
- yandex_py-0.0.1/yandex_py/request_sender/request_sender.py +66 -0
- yandex_py-0.0.1/yandex_py.egg-info/PKG-INFO +47 -0
- yandex_py-0.0.1/yandex_py.egg-info/SOURCES.txt +20 -0
- yandex_py-0.0.1/yandex_py.egg-info/dependency_links.txt +1 -0
- yandex_py-0.0.1/yandex_py.egg-info/requires.txt +2 -0
- yandex_py-0.0.1/yandex_py.egg-info/top_level.txt +1 -0
yandex_py-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: yandex_py
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Yandex API wrapper for Python built on top of httpx and pydantic
|
|
5
|
+
Author-email: "Fiodar Y." <hi@fiodar.dev>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.13
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: httpx>=0.28.1
|
|
10
|
+
Requires-Dist: pydantic>=2.0
|
|
11
|
+
|
|
12
|
+
# yandex_py
|
|
13
|
+
|
|
14
|
+
Python-клиент для Yandex Direct API на базе httpx и pydantic.
|
|
15
|
+
|
|
16
|
+
## Установка
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install yandex_py
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Использование
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from yandex_py import HTTPRequestSender, YDirectReport
|
|
26
|
+
from yandex_py.reports.schemas import (
|
|
27
|
+
DateRangeType, FieldName, Page,
|
|
28
|
+
ReportDefinition, ReportRequest, ReportType, SelectionCriteria,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
sender = HTTPRequestSender(token="your_token", client_login="your_login")
|
|
32
|
+
|
|
33
|
+
request = ReportRequest(
|
|
34
|
+
params=ReportDefinition(
|
|
35
|
+
selection_criteria=SelectionCriteria(),
|
|
36
|
+
field_names=[FieldName.Date, FieldName.Clicks, FieldName.Cost],
|
|
37
|
+
report_name="my-report",
|
|
38
|
+
report_type=ReportType.CUSTOM_REPORT,
|
|
39
|
+
date_range_type=DateRangeType.LAST_7_DAYS,
|
|
40
|
+
page=Page(limit=10000),
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
async with sender:
|
|
45
|
+
report = YDirectReport(request=request, sender=sender)
|
|
46
|
+
rows = await report.fetch()
|
|
47
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# yandex_py
|
|
2
|
+
|
|
3
|
+
Python-клиент для Yandex Direct API на базе httpx и pydantic.
|
|
4
|
+
|
|
5
|
+
## Установка
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install yandex_py
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Использование
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from yandex_py import HTTPRequestSender, YDirectReport
|
|
15
|
+
from yandex_py.reports.schemas import (
|
|
16
|
+
DateRangeType, FieldName, Page,
|
|
17
|
+
ReportDefinition, ReportRequest, ReportType, SelectionCriteria,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
sender = HTTPRequestSender(token="your_token", client_login="your_login")
|
|
21
|
+
|
|
22
|
+
request = ReportRequest(
|
|
23
|
+
params=ReportDefinition(
|
|
24
|
+
selection_criteria=SelectionCriteria(),
|
|
25
|
+
field_names=[FieldName.Date, FieldName.Clicks, FieldName.Cost],
|
|
26
|
+
report_name="my-report",
|
|
27
|
+
report_type=ReportType.CUSTOM_REPORT,
|
|
28
|
+
date_range_type=DateRangeType.LAST_7_DAYS,
|
|
29
|
+
page=Page(limit=10000),
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
async with sender:
|
|
34
|
+
report = YDirectReport(request=request, sender=sender)
|
|
35
|
+
rows = await report.fetch()
|
|
36
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "yandex_py"
|
|
3
|
+
description = "Yandex API wrapper for Python built on top of httpx and pydantic"
|
|
4
|
+
authors = [
|
|
5
|
+
{ name = "Fiodar Y.", email = "hi@fiodar.dev" },
|
|
6
|
+
]
|
|
7
|
+
|
|
8
|
+
version = "0.0.1"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.13"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"httpx>=0.28.1",
|
|
14
|
+
"pydantic>=2.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[dependency-groups]
|
|
18
|
+
dev = [
|
|
19
|
+
"ruff",
|
|
20
|
+
"pytest",
|
|
21
|
+
"pytest-asyncio",
|
|
22
|
+
"model-bakery",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[tool.pytest.ini_options]
|
|
26
|
+
addopts = "-m 'not integration'"
|
|
27
|
+
asyncio_mode = "auto"
|
|
28
|
+
markers = [
|
|
29
|
+
"integration: tests against real Yandex Direct sandbox API (deselected by default)",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[tool.uv]
|
|
33
|
+
package = true
|
|
34
|
+
|
|
35
|
+
[[tool.uv.index]]
|
|
36
|
+
name = "pypi"
|
|
37
|
+
url = "https://pypi.org/simple"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from yandex_py import HTTPRequestSender, YDirectReport
|
|
6
|
+
from yandex_py.constants import SANDBOX_URL
|
|
7
|
+
from yandex_py.reports.schemas import (
|
|
8
|
+
DateRangeType,
|
|
9
|
+
FieldName,
|
|
10
|
+
Page,
|
|
11
|
+
ReportDefinition,
|
|
12
|
+
ReportRequest,
|
|
13
|
+
ReportType,
|
|
14
|
+
SelectionCriteria,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def sender():
|
|
20
|
+
token = os.environ.get("YANDEX_DIRECT_API_TOKEN")
|
|
21
|
+
client_login = os.environ.get("YANDEX_DIRECT_CLIENT_LOGIN")
|
|
22
|
+
|
|
23
|
+
if not token:
|
|
24
|
+
pytest.skip("YANDEX_DIRECT_API_TOKEN не задан")
|
|
25
|
+
if not client_login:
|
|
26
|
+
pytest.skip("YANDEX_DIRECT_CLIENT_LOGIN не задан")
|
|
27
|
+
|
|
28
|
+
return HTTPRequestSender(token=token, client_login=client_login)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture
|
|
32
|
+
def basic_request():
|
|
33
|
+
return ReportRequest(
|
|
34
|
+
params=ReportDefinition(
|
|
35
|
+
selection_criteria=SelectionCriteria(),
|
|
36
|
+
field_names=[FieldName.Date, FieldName.Clicks, FieldName.Impressions, FieldName.Cost],
|
|
37
|
+
report_name="sandbox-test",
|
|
38
|
+
report_type=ReportType.CUSTOM_REPORT,
|
|
39
|
+
date_range_type=DateRangeType.LAST_7_DAYS,
|
|
40
|
+
page=Page(limit=10),
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@pytest.mark.integration
|
|
46
|
+
def test_fetch_report_sync(sender, basic_request):
|
|
47
|
+
report = YDirectReport(request=basic_request, sender=sender)
|
|
48
|
+
rows = report.fetch_sync()
|
|
49
|
+
|
|
50
|
+
assert isinstance(rows, list)
|
|
51
|
+
if rows:
|
|
52
|
+
assert FieldName.Date in rows[0]
|
|
53
|
+
assert FieldName.Clicks in rows[0]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@pytest.mark.integration
|
|
57
|
+
async def test_fetch_report_async(sender, basic_request):
|
|
58
|
+
report = YDirectReport(request=basic_request, sender=sender)
|
|
59
|
+
rows = await report.fetch()
|
|
60
|
+
|
|
61
|
+
assert isinstance(rows, list)
|
|
62
|
+
if rows:
|
|
63
|
+
assert FieldName.Date in rows[0]
|
|
64
|
+
assert FieldName.Clicks in rows[0]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
SANDBOX_URL = "https://api-sandbox.direct.yandex.com/json/v5"
|
|
2
|
+
PRODUCTION_URL = "https://api.direct.yandex.com/json/v5"
|
|
3
|
+
|
|
4
|
+
BASE_URL = PRODUCTION_URL
|
|
5
|
+
|
|
6
|
+
REPORTS_SERVICE = "reports"
|
|
7
|
+
|
|
8
|
+
# Значение отсутствующих данных в TSV-ответе Яндекс.Директ
|
|
9
|
+
MISSING_VALUE = "--"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import io
|
|
3
|
+
|
|
4
|
+
from yandex_py.constants import MISSING_VALUE
|
|
5
|
+
from yandex_py.reports.schemas.fields import FieldName
|
|
6
|
+
|
|
7
|
+
ReportRow = dict[FieldName, str | None]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def parse_report(tsv: str) -> list[ReportRow]:
|
|
11
|
+
"""
|
|
12
|
+
Парсит TSV-ответ API Яндекс.Директ в список словарей.
|
|
13
|
+
|
|
14
|
+
Ожидает ответ с skipReportHeader=true и skipReportSummary=true.
|
|
15
|
+
Значение '--' (отсутствующие данные) преобразуется в None.
|
|
16
|
+
"""
|
|
17
|
+
reader = csv.DictReader(io.StringIO(tsv.strip()), delimiter="\t")
|
|
18
|
+
|
|
19
|
+
if reader.fieldnames is None:
|
|
20
|
+
return []
|
|
21
|
+
|
|
22
|
+
unknown = [f for f in reader.fieldnames if f not in FieldName._value2member_map_]
|
|
23
|
+
if unknown:
|
|
24
|
+
raise ValueError(f"Неизвестные поля в ответе: {unknown}")
|
|
25
|
+
|
|
26
|
+
rows: list[ReportRow] = []
|
|
27
|
+
for raw_row in reader:
|
|
28
|
+
row: ReportRow = {
|
|
29
|
+
FieldName(key): (None if value == MISSING_VALUE else value)
|
|
30
|
+
for key, value in raw_row.items()
|
|
31
|
+
if key is not None
|
|
32
|
+
}
|
|
33
|
+
rows.append(row)
|
|
34
|
+
|
|
35
|
+
return rows
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
from yandex_py.constants import REPORTS_SERVICE
|
|
5
|
+
from yandex_py.reports.parser import ReportRow, parse_report
|
|
6
|
+
from yandex_py.reports.schemas.headers import AcceptLanguage, ProcessingMode
|
|
7
|
+
from yandex_py.reports.schemas.request import ReportRequest
|
|
8
|
+
from yandex_py.request_sender.request_sender import HTTPRequestSender
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class YDirectReport:
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
request: ReportRequest,
|
|
15
|
+
sender: HTTPRequestSender,
|
|
16
|
+
processing_mode: ProcessingMode = ProcessingMode.auto,
|
|
17
|
+
accept_language: AcceptLanguage = AcceptLanguage.ru,
|
|
18
|
+
max_retries: int = 100,
|
|
19
|
+
):
|
|
20
|
+
self._request = request
|
|
21
|
+
self._sender = sender
|
|
22
|
+
self._headers = {
|
|
23
|
+
"Accept-Language": accept_language.value,
|
|
24
|
+
"processingMode": processing_mode.value,
|
|
25
|
+
"returnMoneyInMicros": "false",
|
|
26
|
+
"skipReportHeader": "true",
|
|
27
|
+
"skipColumnHeader": "false",
|
|
28
|
+
"skipReportSummary": "true",
|
|
29
|
+
"Accept-Encoding": "gzip",
|
|
30
|
+
}
|
|
31
|
+
self._max_retries = max_retries
|
|
32
|
+
|
|
33
|
+
def _body(self) -> dict:
|
|
34
|
+
return self._request.model_dump(by_alias=True, exclude_none=True)
|
|
35
|
+
|
|
36
|
+
def _handle_response(self, response) -> list[ReportRow] | None:
|
|
37
|
+
if response.status_code == 200:
|
|
38
|
+
return parse_report(response.text)
|
|
39
|
+
if response.status_code in (201, 202):
|
|
40
|
+
return None
|
|
41
|
+
raise RuntimeError(f"HTTP {response.status_code}: {response.text}")
|
|
42
|
+
|
|
43
|
+
async def fetch(self) -> list[ReportRow]:
|
|
44
|
+
body = self._body()
|
|
45
|
+
for _ in range(self._max_retries):
|
|
46
|
+
response = await self._sender.post_async(REPORTS_SERVICE, body, self._headers)
|
|
47
|
+
result = self._handle_response(response)
|
|
48
|
+
if result is not None:
|
|
49
|
+
return result
|
|
50
|
+
retry_in = int(response.headers.get("retryIn", 10))
|
|
51
|
+
await asyncio.sleep(retry_in)
|
|
52
|
+
raise TimeoutError(f"Отчёт не сформирован за {self._max_retries} попыток")
|
|
53
|
+
|
|
54
|
+
def fetch_sync(self) -> list[ReportRow]:
|
|
55
|
+
body = self._body()
|
|
56
|
+
for _ in range(self._max_retries):
|
|
57
|
+
response = self._sender.post(REPORTS_SERVICE, body, self._headers)
|
|
58
|
+
result = self._handle_response(response)
|
|
59
|
+
if result is not None:
|
|
60
|
+
return result
|
|
61
|
+
retry_in = int(response.headers.get("retryIn", 10))
|
|
62
|
+
time.sleep(retry_in)
|
|
63
|
+
raise TimeoutError(f"Отчёт не сформирован за {self._max_retries} попыток")
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from .fields import FieldName
|
|
2
|
+
from .headers import AcceptLanguage, Headers, ProcessingMode
|
|
3
|
+
from .request import (
|
|
4
|
+
AttributionModel,
|
|
5
|
+
FilterItem,
|
|
6
|
+
FilterOperator,
|
|
7
|
+
OrderBy,
|
|
8
|
+
Page,
|
|
9
|
+
ReportDefinition,
|
|
10
|
+
ReportRequest,
|
|
11
|
+
ReportType,
|
|
12
|
+
SelectionCriteria,
|
|
13
|
+
SortOrder,
|
|
14
|
+
YesNo,
|
|
15
|
+
)
|
|
16
|
+
from .time_period import DateRangeType, TimePeriod
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"FieldName",
|
|
20
|
+
"AcceptLanguage",
|
|
21
|
+
"Headers",
|
|
22
|
+
"ProcessingMode",
|
|
23
|
+
"AttributionModel",
|
|
24
|
+
"FilterItem",
|
|
25
|
+
"FilterOperator",
|
|
26
|
+
"OrderBy",
|
|
27
|
+
"Page",
|
|
28
|
+
"ReportDefinition",
|
|
29
|
+
"ReportRequest",
|
|
30
|
+
"ReportType",
|
|
31
|
+
"SelectionCriteria",
|
|
32
|
+
"SortOrder",
|
|
33
|
+
"YesNo",
|
|
34
|
+
"DateRangeType",
|
|
35
|
+
"TimePeriod",
|
|
36
|
+
]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class FieldName(str, Enum):
|
|
5
|
+
AdFormat = "AdFormat"
|
|
6
|
+
AdGroupId = "AdGroupId"
|
|
7
|
+
AdGroupName = "AdGroupName"
|
|
8
|
+
AdId = "AdId"
|
|
9
|
+
AdUrl = "AdUrl"
|
|
10
|
+
AdNetworkType = "AdNetworkType"
|
|
11
|
+
Age = "Age"
|
|
12
|
+
AudienceTargetId = "AudienceTargetId"
|
|
13
|
+
AvgClickPosition = "AvgClickPosition"
|
|
14
|
+
AvgCpc = "AvgCpc"
|
|
15
|
+
AvgCpm = "AvgCpm"
|
|
16
|
+
AvgEffectiveBid = "AvgEffectiveBid"
|
|
17
|
+
AvgImpressionFrequency = "AvgImpressionFrequency"
|
|
18
|
+
AvgImpressionPosition = "AvgImpressionPosition"
|
|
19
|
+
AvgPageviews = "AvgPageviews"
|
|
20
|
+
AvgTrafficVolume = "AvgTrafficVolume"
|
|
21
|
+
AvgVideoCompleteCost = "AvgVideoCompleteCost"
|
|
22
|
+
AutotargetingBrandOption = "AutotargetingBrandOption"
|
|
23
|
+
BounceRate = "BounceRate"
|
|
24
|
+
Bounces = "Bounces"
|
|
25
|
+
CPV = "CPV"
|
|
26
|
+
CampaignId = "CampaignId"
|
|
27
|
+
CampaignName = "CampaignName"
|
|
28
|
+
CampaignType = "CampaignType"
|
|
29
|
+
CampaignUrlPath = "CampaignUrlPath"
|
|
30
|
+
CarrierType = "CarrierType"
|
|
31
|
+
ClickType = "ClickType"
|
|
32
|
+
Clicks = "Clicks"
|
|
33
|
+
ClientLogin = "ClientLogin"
|
|
34
|
+
ConversionRate = "ConversionRate"
|
|
35
|
+
Conversions = "Conversions"
|
|
36
|
+
Cost = "Cost"
|
|
37
|
+
CostPerConversion = "CostPerConversion"
|
|
38
|
+
Criteria = "Criteria"
|
|
39
|
+
CriteriaId = "CriteriaId"
|
|
40
|
+
CriteriaType = "CriteriaType"
|
|
41
|
+
Criterion = "Criterion"
|
|
42
|
+
CriterionId = "CriterionId"
|
|
43
|
+
CriterionType = "CriterionType"
|
|
44
|
+
Ctr = "Ctr"
|
|
45
|
+
Date = "Date"
|
|
46
|
+
Device = "Device"
|
|
47
|
+
DynamicTextAdTargetId = "DynamicTextAdTargetId"
|
|
48
|
+
ExternalNetworkName = "ExternalNetworkName"
|
|
49
|
+
Gender = "Gender"
|
|
50
|
+
GoalsRoi = "GoalsRoi"
|
|
51
|
+
ImpressionReach = "ImpressionReach"
|
|
52
|
+
ImpressionShare = "ImpressionShare"
|
|
53
|
+
Impressions = "Impressions"
|
|
54
|
+
IncomeGrade = "IncomeGrade"
|
|
55
|
+
Keyword = "Keyword"
|
|
56
|
+
LocationOfPresenceId = "LocationOfPresenceId"
|
|
57
|
+
LocationOfPresenceName = "LocationOfPresenceName"
|
|
58
|
+
MatchType = "MatchType"
|
|
59
|
+
MatchedKeyword = "MatchedKeyword"
|
|
60
|
+
MobilePlatform = "MobilePlatform"
|
|
61
|
+
Month = "Month"
|
|
62
|
+
Placement = "Placement"
|
|
63
|
+
Profit = "Profit"
|
|
64
|
+
PurchaseGoals = "PurchaseGoals"
|
|
65
|
+
PurchaseGoalsRoi = "PurchaseGoalsRoi"
|
|
66
|
+
PurchaseProfit = "PurchaseProfit"
|
|
67
|
+
PurchaseRevenue = "PurchaseRevenue"
|
|
68
|
+
Quarter = "Quarter"
|
|
69
|
+
Query = "Query"
|
|
70
|
+
Revenue = "Revenue"
|
|
71
|
+
RlAdjustmentId = "RlAdjustmentId"
|
|
72
|
+
Sessions = "Sessions"
|
|
73
|
+
Slot = "Slot"
|
|
74
|
+
SmartAdTargetId = "SmartAdTargetId"
|
|
75
|
+
SmartBannerFilterId = "SmartBannerFilterId"
|
|
76
|
+
TargetingCategory = "TargetingCategory"
|
|
77
|
+
TargetingLocationId = "TargetingLocationId"
|
|
78
|
+
TargetingLocationName = "TargetingLocationName"
|
|
79
|
+
VideoComplete = "VideoComplete"
|
|
80
|
+
VideoCompleteRate = "VideoCompleteRate"
|
|
81
|
+
VideoFirstQuartile = "VideoFirstQuartile"
|
|
82
|
+
VideoFirstQuartileRate = "VideoFirstQuartileRate"
|
|
83
|
+
VideoMidpoint = "VideoMidpoint"
|
|
84
|
+
VideoMidpointRate = "VideoMidpointRate"
|
|
85
|
+
VideoThirdQuartile = "VideoThirdQuartile"
|
|
86
|
+
VideoThirdQuartileRate = "VideoThirdQuartileRate"
|
|
87
|
+
VideoViews = "VideoViews"
|
|
88
|
+
VideoViewsRate = "VideoViewsRate"
|
|
89
|
+
Views = "Views"
|
|
90
|
+
Week = "Week"
|
|
91
|
+
WeightedCtr = "WeightedCtr"
|
|
92
|
+
WeightedImpressions = "WeightedImpressions"
|
|
93
|
+
Year = "Year"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ProcessingMode(str, Enum):
|
|
7
|
+
"""
|
|
8
|
+
Режим обработки отчета.
|
|
9
|
+
- auto — Сервер автоматически выбирает режим формирования отчета. Если отчет успешно сформирован в режиме онлайн,
|
|
10
|
+
он будет передан в теле ответа. Если отчет не может быть сформирован в режиме онлайн,
|
|
11
|
+
он ставится в очередь на формирование в режиме офлайн. Приложение должно поддерживать оба режима.
|
|
12
|
+
Отсутствие заголовка эквивалентно значению auto.
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
- offline — Отчет ставится в очередь на формирование в режиме офлайн. Проверять готовность отчета нужно
|
|
16
|
+
с помощью повторных запросов с теми же параметрами, см. подраздел Как проверить готовность офлайн-отчета.
|
|
17
|
+
Рекомендуемый интервал проверки указан в HTTP-заголовке ответа retryIn. Если формирование отчета завершено успешно,
|
|
18
|
+
сервер возвращает отчет в теле ответа.
|
|
19
|
+
|
|
20
|
+
- online — Отчет формируется в режиме онлайн. Если отчет успешно сформирован, он будет передан в
|
|
21
|
+
теле ответа. Если отчет не может быть сформирован в режиме онлайн, возвращается ошибка.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
auto = "auto"
|
|
27
|
+
offline = "offline"
|
|
28
|
+
online = "online"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AcceptLanguage(str, Enum):
|
|
32
|
+
"""
|
|
33
|
+
Язык, на котором возвращаются названия полей в отчете. Допустимые значения: ru, en. Если заголовок не указан, названия полей возвращаются на
|
|
34
|
+
русском языке.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
ru = "ru"
|
|
38
|
+
en = "en"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Headers(BaseModel):
|
|
42
|
+
"""
|
|
43
|
+
Заголовки запроса
|
|
44
|
+
|
|
45
|
+
В дополнение к заголовкам Authorization, Accept-Language, Client-Login можно указать следующие заголовки:
|
|
46
|
+
|
|
47
|
+
authorization
|
|
48
|
+
Заголовок Authorization должен содержать строку вида Bearer <токен>, где <токен> — это токен доступа, полученный в результате аутентификации. Подробнее см. раздел Получение токена доступа.
|
|
49
|
+
|
|
50
|
+
accept_language: "en"
|
|
51
|
+
Язык, на котором возвращаются названия полей в отчете. Допустимые значения: ru, en. Если заголовок не указан, названия полей возвращаются на
|
|
52
|
+
русском языке.
|
|
53
|
+
|
|
54
|
+
client_login
|
|
55
|
+
Логин рекламодателя — клиента рекламного агентства. Обязателен, если запрос осуществляется от имени агентства.
|
|
56
|
+
|
|
57
|
+
processing_mode: "auto"
|
|
58
|
+
Режим формирования отчета: online, offline или auto. Отсутствие заголовка эквивалентно значению auto. Описание режимов приведено в разделе Онлайн- и офлайн-отчет.
|
|
59
|
+
|
|
60
|
+
return_money_in_micros
|
|
61
|
+
Если заголовок указан, денежные значения в отчете возвращаются в валюте с точностью до двух знаков после запятой. Если не указан, денежные значения возвращаются в виде целых чисел — сумм в валюте, умноженных на 1 000 000.
|
|
62
|
+
|
|
63
|
+
skup_report_header: True
|
|
64
|
+
Не выводить в отчете строку с названием отчета и диапазоном дат.
|
|
65
|
+
|
|
66
|
+
skip_column_header: False
|
|
67
|
+
Не выводить в отчете строку с названиями полей.
|
|
68
|
+
|
|
69
|
+
skip_report_summary: True
|
|
70
|
+
Не выводить в отчете строку с количеством строк статистики.
|
|
71
|
+
|
|
72
|
+
accept_encoding: "gzip"
|
|
73
|
+
Если заголовок указан, применяется сжатие gzip.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
authorization: str = Field(..., alias="Authorization")
|
|
77
|
+
client_login: str = Field(..., alias="Client-Login")
|
|
78
|
+
accept_language: AcceptLanguage = Field(AcceptLanguage.ru, alias="Accept-Language")
|
|
79
|
+
processing_mode: str = Field("auto", alias="processingMode")
|
|
80
|
+
return_money_in_micros: bool = Field(False, alias="returnMoneyInMicros")
|
|
81
|
+
skip_report_header: bool = Field(True, alias="skipReportHeader")
|
|
82
|
+
skip_column_header: bool = Field(False, alias="skipColumnHeader")
|
|
83
|
+
skip_report_summary: bool = Field(True, alias="skipReportSummary")
|
|
84
|
+
accept_encoding: str = Field("gzip", alias="Accept-Encoding")
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Literal, Optional
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
5
|
+
|
|
6
|
+
from yandex_py.reports.schemas.fields import FieldName
|
|
7
|
+
from yandex_py.reports.schemas.time_period import DateRangeType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ReportType(str, Enum):
|
|
11
|
+
ACCOUNT_PERFORMANCE_REPORT = "ACCOUNT_PERFORMANCE_REPORT"
|
|
12
|
+
ADGROUP_PERFORMANCE_REPORT = "ADGROUP_PERFORMANCE_REPORT"
|
|
13
|
+
AD_PERFORMANCE_REPORT = "AD_PERFORMANCE_REPORT"
|
|
14
|
+
CAMPAIGN_PERFORMANCE_REPORT = "CAMPAIGN_PERFORMANCE_REPORT"
|
|
15
|
+
CRITERIA_PERFORMANCE_REPORT = "CRITERIA_PERFORMANCE_REPORT"
|
|
16
|
+
CUSTOM_REPORT = "CUSTOM_REPORT"
|
|
17
|
+
REACH_AND_FREQUENCY_PERFORMANCE_REPORT = "REACH_AND_FREQUENCY_PERFORMANCE_REPORT"
|
|
18
|
+
SEARCH_QUERY_PERFORMANCE_REPORT = "SEARCH_QUERY_PERFORMANCE_REPORT"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class FilterOperator(str, Enum):
|
|
22
|
+
EQUALS = "EQUALS"
|
|
23
|
+
NOT_EQUALS = "NOT_EQUALS"
|
|
24
|
+
IN = "IN"
|
|
25
|
+
NOT_IN = "NOT_IN"
|
|
26
|
+
LESS_THAN = "LESS_THAN"
|
|
27
|
+
GREATER_THAN = "GREATER_THAN"
|
|
28
|
+
STARTS_WITH_IGNORE_CASE = "STARTS_WITH_IGNORE_CASE"
|
|
29
|
+
DOES_NOT_START_WITH_IGNORE_CASE = "DOES_NOT_START_WITH_IGNORE_CASE"
|
|
30
|
+
STARTS_WITH_ANY_IGNORE_CASE = "STARTS_WITH_ANY_IGNORE_CASE"
|
|
31
|
+
DOES_NOT_START_WITH_ALL_IGNORE_CASE = "DOES_NOT_START_WITH_ALL_IGNORE_CASE"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AttributionModel(str, Enum):
|
|
35
|
+
FC = "FC" # first click
|
|
36
|
+
LC = "LC" # last click
|
|
37
|
+
LSC = "LSC" # last significant click
|
|
38
|
+
LYDC = "LYDC" # last Yandex Direct click
|
|
39
|
+
FCCD = "FCCD" # first click cross-device
|
|
40
|
+
LSCCD = "LSCCD" # last significant click cross-device
|
|
41
|
+
LYDCCD = "LYDCCD" # last Yandex Direct click cross-device
|
|
42
|
+
AUTO = "AUTO"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SortOrder(str, Enum):
|
|
46
|
+
ASCENDING = "ASCENDING"
|
|
47
|
+
DESCENDING = "DESCENDING"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class YesNo(str, Enum):
|
|
51
|
+
YES = "YES"
|
|
52
|
+
NO = "NO"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class FilterItem(BaseModel):
|
|
56
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
57
|
+
|
|
58
|
+
field: FieldName = Field(..., serialization_alias="Field")
|
|
59
|
+
operator: FilterOperator = Field(..., serialization_alias="Operator")
|
|
60
|
+
values: list[str] = Field(..., serialization_alias="Values")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class SelectionCriteria(BaseModel):
|
|
64
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
65
|
+
|
|
66
|
+
date_from: Optional[str] = Field(None, serialization_alias="DateFrom")
|
|
67
|
+
date_to: Optional[str] = Field(None, serialization_alias="DateTo")
|
|
68
|
+
filter: Optional[list[FilterItem]] = Field(None, serialization_alias="Filter")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Page(BaseModel):
|
|
72
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
73
|
+
|
|
74
|
+
limit: int = Field(..., serialization_alias="Limit")
|
|
75
|
+
offset: Optional[int] = Field(None, serialization_alias="Offset")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class OrderBy(BaseModel):
|
|
79
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
80
|
+
|
|
81
|
+
field: FieldName = Field(..., serialization_alias="Field")
|
|
82
|
+
sort_order: Optional[SortOrder] = Field(None, serialization_alias="SortOrder")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ReportDefinition(BaseModel):
|
|
86
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
87
|
+
|
|
88
|
+
selection_criteria: SelectionCriteria = Field(..., serialization_alias="SelectionCriteria")
|
|
89
|
+
field_names: list[FieldName] = Field(..., serialization_alias="FieldNames")
|
|
90
|
+
report_name: str = Field(..., serialization_alias="ReportName")
|
|
91
|
+
report_type: ReportType = Field(..., serialization_alias="ReportType")
|
|
92
|
+
date_range_type: DateRangeType = Field(..., serialization_alias="DateRangeType")
|
|
93
|
+
format: Literal["TSV"] = Field("TSV", serialization_alias="Format")
|
|
94
|
+
include_vat: YesNo = Field(YesNo.NO, serialization_alias="IncludeVAT")
|
|
95
|
+
include_discount: YesNo = Field(YesNo.NO, serialization_alias="IncludeDiscount")
|
|
96
|
+
goals: Optional[list[str]] = Field(None, serialization_alias="Goals")
|
|
97
|
+
attribution_models: Optional[list[AttributionModel]] = Field(None, serialization_alias="AttributionModels")
|
|
98
|
+
page: Optional[Page] = Field(None, serialization_alias="Page")
|
|
99
|
+
order_by: Optional[list[OrderBy]] = Field(None, serialization_alias="OrderBy")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class ReportRequest(BaseModel):
|
|
103
|
+
"""Тело запроса к API отчётов Яндекс.Директ."""
|
|
104
|
+
|
|
105
|
+
params: ReportDefinition
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from datetime import date
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, model_validator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DateRangeType(str, Enum):
|
|
9
|
+
TODAY = "TODAY"
|
|
10
|
+
YESTERDAY = "YESTERDAY"
|
|
11
|
+
LAST_3_DAYS = "LAST_3_DAYS"
|
|
12
|
+
LAST_5_DAYS = "LAST_5_DAYS"
|
|
13
|
+
LAST_7_DAYS = "LAST_7_DAYS"
|
|
14
|
+
LAST_14_DAYS = "LAST_14_DAYS"
|
|
15
|
+
LAST_30_DAYS = "LAST_30_DAYS"
|
|
16
|
+
LAST_90_DAYS = "LAST_90_DAYS"
|
|
17
|
+
LAST_365_DAYS = "LAST_365_DAYS"
|
|
18
|
+
THIS_WEEK_MON_TODAY = "THIS_WEEK_MON_TODAY"
|
|
19
|
+
THIS_WEEK_SUN_TODAY = "THIS_WEEK_SUN_TODAY"
|
|
20
|
+
LAST_WEEK = "LAST_WEEK"
|
|
21
|
+
LAST_BUSINESS_WEEK = "LAST_BUSINESS_WEEK"
|
|
22
|
+
LAST_WEEK_SUN_SAT = "LAST_WEEK_SUN_SAT"
|
|
23
|
+
THIS_MONTH = "THIS_MONTH"
|
|
24
|
+
LAST_MONTH = "LAST_MONTH"
|
|
25
|
+
ALL_TIME = "ALL_TIME"
|
|
26
|
+
CUSTOM_DATE = "CUSTOM_DATE"
|
|
27
|
+
AUTO = "AUTO"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TimePeriod(BaseModel):
|
|
31
|
+
"""
|
|
32
|
+
Период отчета.
|
|
33
|
+
|
|
34
|
+
При DateRangeType=CUSTOM_DATE необходимо указать date_from и date_to.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
date_range_type: DateRangeType
|
|
38
|
+
date_from: Optional[date] = None
|
|
39
|
+
date_to: Optional[date] = None
|
|
40
|
+
|
|
41
|
+
@model_validator(mode="after")
|
|
42
|
+
def validate_custom_date(self) -> "TimePeriod":
|
|
43
|
+
if self.date_range_type == DateRangeType.CUSTOM_DATE:
|
|
44
|
+
if self.date_from is None or self.date_to is None:
|
|
45
|
+
raise ValueError("date_from и date_to обязательны при DateRangeType=CUSTOM_DATE")
|
|
46
|
+
if self.date_from > self.date_to:
|
|
47
|
+
raise ValueError("date_from не может быть позже date_to")
|
|
48
|
+
return self
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
|
|
3
|
+
from yandex_py.constants import BASE_URL
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class HTTPRequestSender:
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
token: str,
|
|
10
|
+
client_login: str,
|
|
11
|
+
base_url: str = BASE_URL,
|
|
12
|
+
client: httpx.Client | None = None,
|
|
13
|
+
async_client: httpx.AsyncClient | None = None,
|
|
14
|
+
):
|
|
15
|
+
self._base_url = base_url
|
|
16
|
+
self._auth_headers = {
|
|
17
|
+
"Authorization": f"Bearer {token}",
|
|
18
|
+
"Client-Login": client_login,
|
|
19
|
+
}
|
|
20
|
+
self._client = client or httpx.Client()
|
|
21
|
+
self._async_client = async_client or httpx.AsyncClient()
|
|
22
|
+
|
|
23
|
+
def _url(self, service: str) -> str:
|
|
24
|
+
return f"{self._base_url}/{service}"
|
|
25
|
+
|
|
26
|
+
def post(
|
|
27
|
+
self,
|
|
28
|
+
service: str,
|
|
29
|
+
body: dict,
|
|
30
|
+
headers: dict[str, str] | None = None,
|
|
31
|
+
) -> httpx.Response:
|
|
32
|
+
return self._client.post(
|
|
33
|
+
self._url(service),
|
|
34
|
+
json=body,
|
|
35
|
+
headers={**self._auth_headers, **(headers or {})},
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
async def post_async(
|
|
39
|
+
self,
|
|
40
|
+
service: str,
|
|
41
|
+
body: dict,
|
|
42
|
+
headers: dict[str, str] | None = None,
|
|
43
|
+
) -> httpx.Response:
|
|
44
|
+
return await self._async_client.post(
|
|
45
|
+
self._url(service),
|
|
46
|
+
json=body,
|
|
47
|
+
headers={**self._auth_headers, **(headers or {})},
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def close(self) -> None:
|
|
51
|
+
self._client.close()
|
|
52
|
+
|
|
53
|
+
async def aclose(self) -> None:
|
|
54
|
+
await self._async_client.aclose()
|
|
55
|
+
|
|
56
|
+
def __enter__(self) -> "HTTPRequestSender":
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
def __exit__(self, *args) -> None:
|
|
60
|
+
self.close()
|
|
61
|
+
|
|
62
|
+
async def __aenter__(self) -> "HTTPRequestSender":
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
async def __aexit__(self, *args) -> None:
|
|
66
|
+
await self.aclose()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: yandex_py
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Yandex API wrapper for Python built on top of httpx and pydantic
|
|
5
|
+
Author-email: "Fiodar Y." <hi@fiodar.dev>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.13
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: httpx>=0.28.1
|
|
10
|
+
Requires-Dist: pydantic>=2.0
|
|
11
|
+
|
|
12
|
+
# yandex_py
|
|
13
|
+
|
|
14
|
+
Python-клиент для Yandex Direct API на базе httpx и pydantic.
|
|
15
|
+
|
|
16
|
+
## Установка
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install yandex_py
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Использование
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from yandex_py import HTTPRequestSender, YDirectReport
|
|
26
|
+
from yandex_py.reports.schemas import (
|
|
27
|
+
DateRangeType, FieldName, Page,
|
|
28
|
+
ReportDefinition, ReportRequest, ReportType, SelectionCriteria,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
sender = HTTPRequestSender(token="your_token", client_login="your_login")
|
|
32
|
+
|
|
33
|
+
request = ReportRequest(
|
|
34
|
+
params=ReportDefinition(
|
|
35
|
+
selection_criteria=SelectionCriteria(),
|
|
36
|
+
field_names=[FieldName.Date, FieldName.Clicks, FieldName.Cost],
|
|
37
|
+
report_name="my-report",
|
|
38
|
+
report_type=ReportType.CUSTOM_REPORT,
|
|
39
|
+
date_range_type=DateRangeType.LAST_7_DAYS,
|
|
40
|
+
page=Page(limit=10000),
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
async with sender:
|
|
45
|
+
report = YDirectReport(request=request, sender=sender)
|
|
46
|
+
rows = await report.fetch()
|
|
47
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
tests/test_sandbox.py
|
|
4
|
+
yandex_py/__init__.py
|
|
5
|
+
yandex_py/constants.py
|
|
6
|
+
yandex_py.egg-info/PKG-INFO
|
|
7
|
+
yandex_py.egg-info/SOURCES.txt
|
|
8
|
+
yandex_py.egg-info/dependency_links.txt
|
|
9
|
+
yandex_py.egg-info/requires.txt
|
|
10
|
+
yandex_py.egg-info/top_level.txt
|
|
11
|
+
yandex_py/reports/__init__.py
|
|
12
|
+
yandex_py/reports/parser.py
|
|
13
|
+
yandex_py/reports/report.py
|
|
14
|
+
yandex_py/reports/schemas/__init__.py
|
|
15
|
+
yandex_py/reports/schemas/fields.py
|
|
16
|
+
yandex_py/reports/schemas/headers.py
|
|
17
|
+
yandex_py/reports/schemas/request.py
|
|
18
|
+
yandex_py/reports/schemas/time_period.py
|
|
19
|
+
yandex_py/request_sender/__init__.py
|
|
20
|
+
yandex_py/request_sender/request_sender.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
yandex_py
|