Skip to content

Commit fe42b08

Browse files
stemirkhanclaude
andcommitted
feat!: API correctness audit v2.0.0 — 10 breaking fixes + CI
Breaking changes (методы исправлены по спецификации API СУЗ 3.0): - send_aggregation: новая структура тела — participantId + aggregationUnits[] (был плоский sntins[]); новый датакласс AggregationUnit - send_dropout: dropout_reason стал обязательным параметром (§4.4.9) - send_surplus: тело переработано по SurplusReport §4.4.12; productGroup → query param - get_quality: orderId стал необязательным; QualityResponse → QualityListResponse - get_quality_cis_list: параметры order_id/gtin → обязательный report_id (§4.4.16) - get_mod: добавлен обязательный product_group; ModResponse → ModListResponse (§4.4.17) - get_receipt_document: добавлен обязательный параметр doc_id (§4.4.20) - search_orders: дефолт page изменён с 0 на 1 (1-based, §4.4.29) Fixed: - search_documents: ключ ответа "results" → "result" (§4.4.21) - BufferInfo.total_codes: тип int → str | int (§4.4.2.2 Table 257) Added: - GitHub Actions CI: ruff + mypy --strict + pytest на Python 3.11/3.12/3.13 540 tests pass, mypy --strict clean, ruff clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ea537f5 commit fe42b08

File tree

13 files changed

+838
-495
lines changed

13 files changed

+838
-495
lines changed

.github/workflows/ci.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: ["main"]
6+
pull_request:
7+
branches: ["main"]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
python-version: ["3.11", "3.12", "3.13"]
15+
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Set up Python ${{ matrix.python-version }}
20+
uses: actions/setup-python@v5
21+
with:
22+
python-version: ${{ matrix.python-version }}
23+
24+
- name: Install dependencies
25+
run: |
26+
python -m pip install --upgrade pip
27+
pip install -e ".[dev]"
28+
29+
- name: Lint with ruff
30+
run: ruff check src/ tests/
31+
32+
- name: Type-check with mypy
33+
run: mypy --strict src/suz_sdk/
34+
35+
- name: Run tests
36+
run: pytest --tb=short

CHANGELOG.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,44 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
1111

1212
---
1313

14+
## [2.0.0] — 2026-03-19
15+
16+
### Breaking Changes
17+
18+
- **`send_aggregation()`** — полностью переработана сигнатура по §4.4.10 (Table 119–120).
19+
Старые параметры `sntins` и `aggregation_type` удалены; добавлены обязательные
20+
`participant_id: str` (ИНН) и `aggregation_units: list[AggregationUnit]`.
21+
Новый датакласс `AggregationUnit` содержит поля `sntins`, `unit_serial_number`,
22+
`aggregated_items_count`, `aggregation_unit_capacity`; поле `aggregationType`
23+
в теле запроса теперь всегда равно `"AGGREGATION"`.
24+
- **`send_dropout()`** — параметр `dropout_reason` стал обязательным (§4.4.9, «Да»).
25+
- **`send_surplus()`** — тело запроса переработано согласно `SurplusReport` §4.4.12
26+
(Table 175): добавлены `document_date`, `participant_inn`, `primary_document_*`
27+
и `codes`; `productGroup` перенесён в query-параметр.
28+
- **`get_quality()`** — параметр `order_id` стал необязательным; модель ответа
29+
переименована `QualityResponse``QualityListResponse`; поле `results` теперь
30+
`list[str]` (UUID отчётов), а не список объектов.
31+
- **`get_quality_cis_list()`** — параметры `order_id`/`gtin` заменены на обязательный
32+
`report_id: str` (UUID из `get_quality()`); модель ответа полностью изменена.
33+
- **`get_mod()`** — добавлен обязательный параметр `product_group: str` (§4.4.17);
34+
модель ответа переименована `ModResponse``ModListResponse`.
35+
- **`get_receipt_document()`** — добавлен обязательный параметр `doc_id: str` (§4.4.20).
36+
- **`search_orders()`** — дефолт `page` изменён с `0` на `1` (1-based, §4.4.29).
37+
38+
### Fixed
39+
40+
- **`search_documents()`** — ключ ответа исправлен с `"results"` на `"result"` (§4.4.21).
41+
- **`BufferInfo.total_codes`** — тип изменён с `int` на `str | int` согласно спецификации
42+
(§4.4.2.2, Table 257, тип «Строка»).
43+
44+
### Added
45+
46+
- Датакласс `AggregationUnit` (публичный, экспортируется из `suz_sdk`).
47+
- GitHub Actions CI (`.github/workflows/ci.yml`) — запуск ruff, mypy, pytest на
48+
Python 3.11/3.12/3.13 при каждом push/PR в `main`.
49+
50+
---
51+
1452
## [0.9.0] — 2026-03-07
1553

1654
### Changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "suz-sdk"
7-
version = "1.2.0"
7+
version = "2.0.0"
88
description = "Python SDK for СУЗ API 3.0 (СУЗ-Облако 4.0)"
99
readme = "README.md"
1010
requires-python = ">=3.11"

src/suz_sdk/__init__.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,14 @@
6161
)
6262
from suz_sdk.api.reference import (
6363
AsyncReferenceApi,
64-
ModResponse,
64+
ModListResponse,
6565
ProvidersResponse,
6666
QualityCisListResponse,
67-
QualityResponse,
67+
QualityListResponse,
6868
ReferenceApi,
6969
)
7070
from suz_sdk.api.reports import (
71+
AggregationUnit,
7172
ReceiptFilter,
7273
ReportsApi,
7374
ReportStatusResponse,
@@ -151,13 +152,14 @@
151152
"SendSurplusResponse",
152153
"ReportStatusResponse",
153154
"SearchReceiptsResponse",
154-
# Request models (reports)
155+
# Request/input models (reports)
156+
"AggregationUnit",
155157
"ReceiptFilter",
156158
# Response models (reference)
157159
"ProvidersResponse",
158-
"QualityResponse",
160+
"QualityListResponse",
159161
"QualityCisListResponse",
160-
"ModResponse",
162+
"ModListResponse",
161163
# Response models (documents)
162164
"SearchDocumentsResponse",
163165
"DeleteDocumentResponse",
@@ -191,4 +193,4 @@
191193
"RetryConfig",
192194
]
193195

194-
__version__ = "1.2.0"
196+
__version__ = "2.0.0"

src/suz_sdk/api/async_reports.py

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from typing import Any, cast
66

77
from suz_sdk.api.reports import (
8+
AggregationUnit,
89
ReceiptFilter,
910
ReportsApi,
1011
ReportStatusResponse,
@@ -79,7 +80,7 @@ async def send_dropout(
7980
self,
8081
product_group: str,
8182
sntins: list[str],
82-
dropout_reason: str | None = None,
83+
dropout_reason: str,
8384
attributes: dict[str, Any] | None = None,
8485
) -> SendDropoutResponse:
8586
"""Send a KM dropout report (POST /api/v3/dropout)."""
@@ -90,9 +91,8 @@ async def send_dropout(
9091
body_dict: dict[str, Any] = {
9192
"productGroup": product_group,
9293
"sntins": sntins,
94+
"dropoutReason": dropout_reason,
9395
}
94-
if dropout_reason is not None:
95-
body_dict["dropoutReason"] = dropout_reason
9696
if attributes is not None:
9797
body_dict["attributes"] = attributes
9898

@@ -120,8 +120,8 @@ async def send_dropout(
120120
async def send_aggregation(
121121
self,
122122
product_group: str,
123-
sntins: list[str],
124-
aggregation_type: str | None = None,
123+
participant_id: str,
124+
aggregation_units: list[AggregationUnit],
125125
attributes: dict[str, Any] | None = None,
126126
) -> SendAggregationResponse:
127127
"""Send a KM aggregation report (POST /api/v3/aggregation)."""
@@ -131,10 +131,18 @@ async def send_aggregation(
131131

132132
body_dict: dict[str, Any] = {
133133
"productGroup": product_group,
134-
"sntins": sntins,
134+
"participantId": participant_id,
135+
"aggregationUnits": [
136+
{
137+
"aggregatedItemsCount": u.aggregated_items_count,
138+
"aggregationType": "AGGREGATION",
139+
"aggregationUnitCapacity": u.aggregation_unit_capacity,
140+
"sntins": u.sntins,
141+
"unitSerialNumber": u.unit_serial_number,
142+
}
143+
for u in aggregation_units
144+
],
135145
}
136-
if aggregation_type is not None:
137-
body_dict["aggregationType"] = aggregation_type
138146
if attributes is not None:
139147
body_dict["attributes"] = attributes
140148

@@ -162,20 +170,36 @@ async def send_aggregation(
162170
async def send_surplus(
163171
self,
164172
product_group: str,
165-
sntins: list[str],
166-
attributes: dict[str, Any] | None = None,
173+
document_date: str,
174+
participant_inn: str,
175+
primary_document_custom_name: str,
176+
primary_document_date: str,
177+
primary_document_number: str,
178+
codes: list[str],
179+
document_type: str = "SURPLUS_POSTING",
180+
document_version: str = "1.0",
181+
participant_kpp: str | None = None,
182+
participant_fias: str | None = None,
167183
) -> SendSurplusResponse:
168-
"""Send a KM surplus report (POST /api/v3/surplus)."""
184+
"""Send a surplus posting notification (POST /api/v3/surplus, §4.4.12)."""
169185
from suz_sdk.transport.async_httpx_transport import AsyncHttpxTransport
170186

171187
transport: AsyncHttpxTransport = self._transport # type: ignore[assignment]
172188

173189
body_dict: dict[str, Any] = {
174-
"productGroup": product_group,
175-
"sntins": sntins,
190+
"documentType": document_type,
191+
"documentVersion": document_version,
192+
"documentDate": document_date,
193+
"participantInn": participant_inn,
194+
"primaryDocumentCustomName": primary_document_custom_name,
195+
"primaryDocumentDate": primary_document_date,
196+
"primaryDocumentNumber": primary_document_number,
197+
"codes": codes,
176198
}
177-
if attributes is not None:
178-
body_dict["attributes"] = attributes
199+
if participant_kpp is not None:
200+
body_dict["participantKpp"] = participant_kpp
201+
if participant_fias is not None:
202+
body_dict["participantFias"] = participant_fias
179203

180204
raw_body = json.dumps(body_dict, ensure_ascii=False).encode()
181205

@@ -190,7 +214,7 @@ async def send_surplus(
190214
req = Request(
191215
method="POST",
192216
path="/api/v3/surplus",
193-
params={"omsId": self._oms_id},
217+
params={"omsId": self._oms_id, "productGroup": product_group},
194218
headers=headers,
195219
raw_body=raw_body,
196220
)

src/suz_sdk/api/documents.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -84,16 +84,19 @@ def __init__(
8484
# Public methods
8585
# ------------------------------------------------------------------
8686

87-
def get_receipt_document(self, result_doc_id: str) -> dict[str, Any]:
88-
"""Retrieve a receipt document by its result doc ID.
87+
def get_receipt_document(self, result_doc_id: str, doc_id: str) -> dict[str, Any]:
88+
"""Retrieve the document associated with a receipt (§4.4.20).
8989
90-
GET /api/v3/receipts/document?omsId={omsId}&resultDocId={resultDocId}
90+
GET /api/v3/receipts/document?omsId={omsId}&resultDocId={resultDocId}&docId={docId}
91+
92+
All three query parameters are required (§4.4.20.1, Table 212).
9193
9294
Args:
93-
result_doc_id: The resultDocId of the receipt document to retrieve.
95+
result_doc_id: UUID of the receipt (order, report, or KM block).
96+
doc_id: UUID of the associated document linked to the receipt.
9497
9598
Returns:
96-
Raw response dict (schema is complex and product-group-specific).
99+
Raw response dict with ``content`` field (base64-encoded document).
97100
98101
Raises:
99102
SuzAuthError: clientToken is missing or invalid.
@@ -107,6 +110,7 @@ def get_receipt_document(self, result_doc_id: str) -> dict[str, Any]:
107110
params={
108111
"omsId": self._oms_id,
109112
"resultDocId": result_doc_id,
113+
"docId": doc_id,
110114
},
111115
headers={
112116
"Accept": "application/json",
@@ -163,7 +167,7 @@ def search_documents(
163167
body: dict[str, Any] = resp.body
164168
return SearchDocumentsResponse(
165169
total_count=body["totalCount"],
166-
results=body.get("results", []),
170+
results=body.get("result", []),
167171
)
168172

169173
def get_document_content(self, doc_id: str) -> dict[str, Any]:
@@ -298,10 +302,10 @@ def __init__(
298302
# Public methods
299303
# ------------------------------------------------------------------
300304

301-
async def get_receipt_document(self, result_doc_id: str) -> dict[str, Any]:
302-
"""Retrieve a receipt document by its result doc ID.
305+
async def get_receipt_document(self, result_doc_id: str, doc_id: str) -> dict[str, Any]:
306+
"""Retrieve the document associated with a receipt (§4.4.20).
303307
304-
GET /api/v3/receipts/document?omsId={omsId}&resultDocId={resultDocId}
308+
GET /api/v3/receipts/document?omsId={omsId}&resultDocId={resultDocId}&docId={docId}
305309
"""
306310
from suz_sdk.transport.async_httpx_transport import AsyncHttpxTransport
307311

@@ -312,6 +316,7 @@ async def get_receipt_document(self, result_doc_id: str) -> dict[str, Any]:
312316
params={
313317
"omsId": self._oms_id,
314318
"resultDocId": result_doc_id,
319+
"docId": doc_id,
315320
},
316321
headers={
317322
"Accept": "application/json",
@@ -357,7 +362,7 @@ async def search_documents(
357362
body: dict[str, Any] = resp.body
358363
return SearchDocumentsResponse(
359364
total_count=body["totalCount"],
360-
results=body.get("results", []),
365+
results=body.get("result", []),
361366
)
362367

363368
async def get_document_content(self, doc_id: str) -> dict[str, Any]:

src/suz_sdk/api/orders.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ class BufferInfo:
6161
gtin: str
6262
buffer_status: str # ACTIVE | PENDING | REJECTED
6363
available_codes: int
64-
total_codes: int
64+
total_codes: str | int
6565
total_passed: int
6666
unavailable_codes: int
6767
left_in_buffer: int
@@ -467,7 +467,7 @@ def search_orders(
467467
self,
468468
filter: OrderFilter | None = None,
469469
limit: int = 10,
470-
page: int = 0,
470+
page: int = 1,
471471
) -> SearchOrdersResponse:
472472
"""Search orders with optional filtering and pagination.
473473
@@ -476,7 +476,7 @@ def search_orders(
476476
Args:
477477
filter: Optional OrderFilter with search criteria.
478478
limit: Maximum number of results to return.
479-
page: Zero-based page number.
479+
page: 1-based page number (default 1, max 100).
480480
481481
Returns:
482482
SearchOrdersResponse with total_count and a list of OrderSummaryInfo.

0 commit comments

Comments
 (0)