Skip to content

Commit 6f332eb

Browse files
committed
Финальная чистка. Начало проверки ux
1 parent 42507cc commit 6f332eb

93 files changed

Lines changed: 3922 additions & 1806 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/settings.local.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
"WebFetch(domain:medium.com)",
1919
"WebFetch(domain:newsletter.pragmaticengineer.com)",
2020
"Bash(awk '{print $NF}')",
21-
"Bash(grep \"| $\" /Users/n.baryshnikov/Projects/avito_python_api/docs/inventory.md)"
21+
"Bash(grep \"| $\" /Users/n.baryshnikov/Projects/avito_python_api/docs/inventory.md)",
22+
"Bash(grep -r \"def.*:$\" /Users/n.baryshnikov/Projects/avito_python_api/avito --include=\"*.py\")"
2223
]
2324
}
2425
}

CHANGELOG.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on Keep a Changelog,
6+
and this project adheres to Semantic Versioning.
7+
8+
## [Unreleased]
9+
10+
### Changed
11+
- Централизовано выполнение схемы `request + map` через `Transport.request_public_model`.
12+
- Убраны прямые обращения доменных клиентов к `request_json` и приватному `Transport._auth_provider`.
13+
- Секционные клиенты переведены на `@dataclass(slots=True, frozen=True)`.
14+
- Иерархия исключений упрощена до frozen dataclass без кастомного `__setattr__`.
15+
- Публичные сигнатуры `accounts`, `ads`, `autoteka`, `cpa`, `jobs`, `messenger`, `orders`, `promotion`, `ratings` и `realty` переведены с `request`-DTO на keyword-only примитивы и коллекции.
16+
- Transport получил поддержку `Idempotency-Key`; публичные write-методы во всех доменах принимают `idempotency_key`, а dry-run/write-контракт promotion покрыт тестами.
17+
- Во всех доменных пакетах добавлены `enums.py`; `accounts`, `ads`, `autoteka`, `jobs`, `messenger`, `orders`, `promotion`, `ratings`, `realty` и `tariffs` переведены на typed enums с fallback на `UNKNOWN` и warning-логом ровно один раз на неизвестное upstream-значение.
18+
19+
## [1.0.2] - 2026-04-21
20+
21+
### Added
22+
- Первый публичный релиз changelog для `avito-py`.
23+
24+
### Changed
25+
- Зафиксирована базовая структура истории изменений для следующих фаз исправления STYLEGUIDE.

STYLEGUIDE.md

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ All HTTP must go through a single transport layer.
339339
Rules:
340340

341341
- Direct calls to `httpx.get()`/`httpx.post()` inside section clients are forbidden.
342-
- Use `httpx.Client` or `httpx.AsyncClient` as an internal dependency of the transport layer.
342+
- Use `httpx.Client` as an internal dependency of the transport layer.
343343
- Timeouts are set explicitly.
344344
- Authorization headers are injected by the transport/auth layer, not by business methods.
345345
- URL construction, error handling, retries, and logging are concentrated in the transport.
@@ -349,7 +349,6 @@ Recommendation:
349349

350350
- Build a high-quality sync SDK first.
351351
- The SDK is synchronous — this must be explicitly documented in the README and public API.
352-
- An async version should be added as a separate layer, not mixed with sync in the same classes.
353352

354353
### User-Agent and Client Identification
355354

@@ -363,19 +362,6 @@ Recommendation:
363362
- Overrides must not mutate the client or shared state. The transport layer resolves the effective policy as `override or client_default` without writing back.
364363
- The list of supported per-operation overrides is part of the public contract and must be documented on each public method.
365364

366-
## Async and Sync Parity
367-
368-
When an async surface is added it must be a separate, parallel layer, not a retrofit of sync classes.
369-
370-
Rules:
371-
372-
- The async namespace is `avito.aio` with mirrored module paths: `avito.aio.client`, `avito.aio.<domain>.client`.
373-
- Async client classes have the same names as their sync counterparts (`AvitoClient`, `AdClient`, ...) but live in the `aio` namespace.
374-
- Public method names and parameter lists must be identical between sync and async. Only the call-site keyword (`await`) and the context-manager form (`async with`) differ.
375-
- Mixing `async def` and `def` on the same class is forbidden.
376-
- Return types are the same public dataclasses in both layers. `PaginatedList[T]` has an `AsyncPaginatedList[T]` sibling with matching semantics and matching `materialize()`.
377-
- Feature parity between sync and async is part of the public contract: a method that exists in sync must exist in async within the same release, or be explicitly marked sync-only in the documentation.
378-
379365
## Authorization
380366

381367
Authorization must be fully abstracted away from API methods.
@@ -885,6 +871,5 @@ Rules:
885871
- Retrying non-idempotent writes without an idempotency key or an explicit per-operation opt-in.
886872
- Raising on expected "not found" for probe methods (`exists()` must return `False`, not throw).
887873
- Exposing `FakeTransport` only through internal test imports instead of `avito.testing`.
888-
- Mixing sync and async surfaces in the same class or module (`avito.aio` is the only home for async).
889874
- Public methods that swallow upstream `request_id` or retry `attempt` in raised exceptions.
890875
- Documentation that covers only reference or only tutorials (all four Diátaxis modes are mandatory).

avito/__init__.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,35 @@
33
from avito.auth.settings import AuthSettings
44
from avito.client import AvitoClient
55
from avito.config import AvitoSettings
6+
from avito.core.exceptions import (
7+
AuthenticationError,
8+
AuthorizationError,
9+
AvitoError,
10+
ConfigurationError,
11+
ConflictError,
12+
RateLimitError,
13+
ResponseMappingError,
14+
TransportError,
15+
UnsupportedOperationError,
16+
UpstreamApiError,
17+
ValidationError,
18+
)
19+
from avito.core.pagination import PaginatedList
620

7-
__all__ = ("AuthSettings", "AvitoClient", "AvitoSettings")
21+
__all__ = (
22+
"AuthSettings",
23+
"AuthenticationError",
24+
"AuthorizationError",
25+
"AvitoClient",
26+
"AvitoError",
27+
"AvitoSettings",
28+
"ConfigurationError",
29+
"ConflictError",
30+
"PaginatedList",
31+
"RateLimitError",
32+
"ResponseMappingError",
33+
"TransportError",
34+
"UnsupportedOperationError",
35+
"UpstreamApiError",
36+
"ValidationError",
37+
)

avito/accounts/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
"""Пакет accounts."""
22

33
from avito.accounts.domain import Account, AccountHierarchy
4+
from avito.accounts.enums import (
5+
AccountHierarchyRole,
6+
EmployeeItemStatus,
7+
OperationStatus,
8+
OperationType,
9+
)
410
from avito.accounts.models import (
511
AccountActionResult,
612
AccountBalance,
@@ -19,12 +25,16 @@
1925
"AccountActionResult",
2026
"AccountBalance",
2127
"AccountHierarchy",
28+
"AccountHierarchyRole",
2229
"AccountProfile",
2330
"AhUserStatus",
2431
"CompanyPhone",
2532
"CompanyPhonesResult",
2633
"Employee",
2734
"EmployeeItem",
35+
"EmployeeItemStatus",
2836
"EmployeesResult",
2937
"OperationRecord",
38+
"OperationStatus",
39+
"OperationType",
3040
)

avito/accounts/client.py

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
from avito.core.mapping import request_public_model
3232

3333

34-
@dataclass(slots=True)
34+
@dataclass(slots=True, frozen=True)
3535
class AccountsClient:
3636
"""Выполняет HTTP-операции по разделу информации о пользователе."""
3737

@@ -59,18 +59,25 @@ def get_balance(self, *, user_id: int) -> AccountBalance:
5959
mapper=map_account_balance,
6060
)
6161

62-
def get_operations_history(self, request: OperationsHistoryRequest) -> PaginatedList[OperationRecord]:
62+
def get_operations_history(
63+
self,
64+
*,
65+
date_from: str | None = None,
66+
date_to: str | None = None,
67+
limit: int | None = None,
68+
offset: int | None = None,
69+
) -> PaginatedList[OperationRecord]:
6370
"""Получает историю операций пользователя."""
6471

65-
page_size = request.limit or 25
66-
base_offset = request.offset or 0
72+
page_size = limit or 25
73+
base_offset = offset or 0
6774

6875
def fetch_page(page: int | None, _cursor: str | None) -> JsonPage[OperationRecord]:
6976
current_page = page or 1
7077
current_offset = base_offset + (current_page - 1) * page_size
7178
paged_request = OperationsHistoryRequest(
72-
date_from=request.date_from,
73-
date_to=request.date_to,
79+
date_from=date_from,
80+
date_to=date_to,
7481
limit=page_size,
7582
offset=current_offset,
7683
)
@@ -92,7 +99,7 @@ def fetch_page(page: int | None, _cursor: str | None) -> JsonPage[OperationRecor
9299
return Paginator(fetch_page).as_list(first_page=fetch_page(1, None))
93100

94101

95-
@dataclass(slots=True)
102+
@dataclass(slots=True, frozen=True)
96103
class HierarchyClient:
97104
"""Выполняет HTTP-операции по иерархии аккаунтов."""
98105

@@ -131,28 +138,49 @@ def list_company_phones(self) -> CompanyPhonesResult:
131138
mapper=map_company_phones,
132139
)
133140

134-
def link_items(self, request: EmployeeItemLinkRequest) -> AccountActionResult:
141+
def link_items(
142+
self,
143+
*,
144+
employee_id: int,
145+
item_ids: list[int],
146+
source_employee_id: int | None = None,
147+
idempotency_key: str | None = None,
148+
) -> AccountActionResult:
135149
"""Прикрепляет объявления к сотруднику."""
136150

137151
return request_public_model(
138152
self.transport,
139153
"POST",
140154
"/linkItemsV1",
141-
context=RequestContext("accounts.hierarchy.link_items", allow_retry=True),
155+
context=RequestContext(
156+
"accounts.hierarchy.link_items",
157+
allow_retry=idempotency_key is not None,
158+
),
142159
mapper=map_action_result,
143-
json_body=request.to_payload(),
160+
json_body=EmployeeItemLinkRequest(
161+
employee_id=employee_id,
162+
item_ids=item_ids,
163+
source_employee_id=source_employee_id,
164+
).to_payload(),
165+
idempotency_key=idempotency_key,
144166
)
145167

146-
def list_items_by_employee(self, request: EmployeeItemsRequest) -> PaginatedList[EmployeeItem]:
168+
def list_items_by_employee(
169+
self,
170+
*,
171+
employee_id: int,
172+
limit: int | None = None,
173+
offset: int | None = None,
174+
) -> PaginatedList[EmployeeItem]:
147175
"""Получает список объявлений по сотруднику."""
148176

149-
page_size = request.limit or 25
177+
page_size = limit or 25
150178

151179
def fetch_page(page: int | None, _cursor: str | None) -> JsonPage[EmployeeItem]:
152180
current_page = page or 1
153-
current_offset = (request.offset or 0) + (current_page - 1) * page_size
181+
current_offset = (offset or 0) + (current_page - 1) * page_size
154182
paged_request = EmployeeItemsRequest(
155-
employee_id=request.employee_id,
183+
employee_id=employee_id,
156184
limit=page_size,
157185
offset=current_offset,
158186
)

avito/accounts/domain.py

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,8 @@
1414
AhUserStatus,
1515
CompanyPhonesResult,
1616
EmployeeItem,
17-
EmployeeItemLinkRequest,
18-
EmployeeItemsRequest,
1917
EmployeesResult,
2018
OperationRecord,
21-
OperationsHistoryRequest,
2219
)
2320
from avito.core import PaginatedList, ValidationError
2421
from avito.core.domain import DomainObject
@@ -58,12 +55,10 @@ def get_operations_history(
5855
"""Получает историю операций пользователя."""
5956

6057
return AccountsClient(self.transport).get_operations_history(
61-
OperationsHistoryRequest(
62-
date_from=_serialize_datetime(date_from),
63-
date_to=_serialize_datetime(date_to),
64-
limit=limit,
65-
offset=offset,
66-
)
58+
date_from=_serialize_datetime(date_from),
59+
date_to=_serialize_datetime(date_to),
60+
limit=limit,
61+
offset=offset,
6762
)
6863

6964

@@ -94,15 +89,15 @@ def link_items(
9489
employee_id: int,
9590
item_ids: Sequence[int],
9691
source_employee_id: int | None = None,
92+
idempotency_key: str | None = None,
9793
) -> AccountActionResult:
9894
"""Прикрепляет объявления к сотруднику."""
9995

10096
return HierarchyClient(self.transport).link_items(
101-
EmployeeItemLinkRequest(
102-
employee_id=employee_id,
103-
item_ids=list(item_ids),
104-
source_employee_id=source_employee_id,
105-
)
97+
employee_id=employee_id,
98+
item_ids=list(item_ids),
99+
source_employee_id=source_employee_id,
100+
idempotency_key=idempotency_key,
106101
)
107102

108103
def list_items_by_employee(
@@ -115,7 +110,9 @@ def list_items_by_employee(
115110
"""Получает список объявлений сотрудника."""
116111

117112
return HierarchyClient(self.transport).list_items_by_employee(
118-
EmployeeItemsRequest(employee_id=employee_id, limit=limit, offset=offset)
113+
employee_id=employee_id,
114+
limit=limit,
115+
offset=offset,
119116
)
120117

121118

avito/accounts/enums.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""Enum-значения раздела accounts."""
2+
3+
from __future__ import annotations
4+
5+
from enum import Enum
6+
7+
8+
class OperationType(str, Enum):
9+
"""Тип операции по аккаунту."""
10+
11+
UNKNOWN = "__unknown__"
12+
PAYMENT = "payment"
13+
14+
15+
class OperationStatus(str, Enum):
16+
"""Статус операции по аккаунту."""
17+
18+
UNKNOWN = "__unknown__"
19+
DONE = "done"
20+
21+
22+
class AccountHierarchyRole(str, Enum):
23+
"""Роль пользователя в иерархии аккаунтов."""
24+
25+
UNKNOWN = "__unknown__"
26+
MANAGER = "manager"
27+
28+
29+
class EmployeeItemStatus(str, Enum):
30+
"""Статус объявления сотрудника."""
31+
32+
UNKNOWN = "__unknown__"
33+
ACTIVE = "active"
34+
35+
36+
__all__ = (
37+
"AccountHierarchyRole",
38+
"EmployeeItemStatus",
39+
"OperationStatus",
40+
"OperationType",
41+
)

0 commit comments

Comments
 (0)