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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,7 @@
1
+ from .request_sender.request_sender import HTTPRequestSender
2
+ from .reports.report import YDirectReport
3
+
4
+ __all__ = [
5
+ "HTTPRequestSender",
6
+ "YDirectReport",
7
+ ]
@@ -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,8 @@
1
+ from .parser import ReportRow, parse_report
2
+ from .report import YDirectReport
3
+
4
+ __all__ = [
5
+ "YDirectReport",
6
+ "parse_report",
7
+ "ReportRow",
8
+ ]
@@ -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,3 @@
1
+ from .request_sender import HTTPRequestSender
2
+
3
+ __all__ = ["HTTPRequestSender"]
@@ -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,2 @@
1
+ httpx>=0.28.1
2
+ pydantic>=2.0
@@ -0,0 +1 @@
1
+ yandex_py