contract: v1.1 — OsvRow.extra, CONTRACT_VERSION 1.1, копия схемы
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
parent
f03bfca92d
commit
947d2a6a51
@ -40,7 +40,7 @@ CORS, регистрация SMB-сессии, импорт `raisa_fastapi_prote
|
||||
**Добавлено** (в скобках — задача `openspec/changes/slice-0-skeleton/tasks.md`):
|
||||
- `api/src/parser/` — интерфейс `Parser` и заглушка `NullParser`; разбор ОСВ §1 и форм §12 — чистые функции над
|
||||
книгой Excel, Срезы 2+;
|
||||
- `api/src/contract/` — pydantic-модели контракта v1.0 (запрос, результат, конверт); `api/contracts/` — копии схем (идентичность с
|
||||
- `api/src/contract/` — pydantic-модели контракта v1.1 (запрос, результат, конверт); `api/contracts/` — копии схем (идентичность с
|
||||
`../docs/contracts` проверяется в `make check`) — 3.1;
|
||||
- `api/rules/` — README о назначении; сами словари `*.yaml` (в документах `rules/*.yaml` относительно `api/`)
|
||||
появятся в Срезе 2 — 4.1;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"schema_version": "1.1",
|
||||
"job_id": "3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01",
|
||||
"file_id": "a1c4e7d2-5b36-4f89-8e12-9d0c3b7a4e02",
|
||||
"service": { "name": "tabular-parser", "version": "0.1.0" },
|
||||
@ -66,7 +66,8 @@
|
||||
"turnover_debit": "500.00",
|
||||
"turnover_credit": "200.50",
|
||||
"closing_debit": "0.00",
|
||||
"closing_credit": "700.50"
|
||||
"closing_credit": "700.50",
|
||||
"extra": { "КПП": "352801001" }
|
||||
}
|
||||
],
|
||||
"checks": {
|
||||
|
||||
99
api/contracts/examples/result-osv-needs-check.json
Normal file
99
api/contracts/examples/result-osv-needs-check.json
Normal file
@ -0,0 +1,99 @@
|
||||
{
|
||||
"schema_version": "1.1",
|
||||
"job_id": "3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01",
|
||||
"file_id": "a1c4e7d2-5b36-4f89-8e12-9d0c3b7a4e02",
|
||||
"service": { "name": "tabular-parser", "version": "0.1.0" },
|
||||
"status": "needs_check",
|
||||
"metrics": { "rows": 3, "warnings": ["hierarchy=outline", "balance identity violated at rows [3]"] },
|
||||
"documents": [
|
||||
{
|
||||
"document_type": "OSV",
|
||||
"locator": "sheet:Лист_1",
|
||||
"header": {
|
||||
"organization_name": "ООО Ромашка",
|
||||
"organization_inn": "7700000000",
|
||||
"account_code_raw": "60",
|
||||
"period_text": "1 квартал 2025 г.",
|
||||
"period_year": 2025,
|
||||
"period_quarter": "Q1",
|
||||
"is_cumulative": false,
|
||||
"data_type": "BU",
|
||||
"currency": "RUB",
|
||||
"header_variant": 1
|
||||
},
|
||||
"group_schema": [
|
||||
{ "type": "COUNTERPARTY", "name": "Контрагенты", "has_inn": true }
|
||||
],
|
||||
"columns": {
|
||||
"opening_debit": "C",
|
||||
"opening_credit": "D",
|
||||
"turnover_debit": "E",
|
||||
"turnover_credit": "F",
|
||||
"closing_debit": "G",
|
||||
"closing_credit": "H"
|
||||
},
|
||||
"rows": [
|
||||
{
|
||||
"row_number": 1,
|
||||
"level": 0,
|
||||
"parent_row_number": null,
|
||||
"row_value": "60",
|
||||
"group_type": "ACCOUNT",
|
||||
"group_name": null,
|
||||
"account_code_raw": "60",
|
||||
"counterparty_name": null,
|
||||
"counterparty_inn": null,
|
||||
"data_type": "BU",
|
||||
"opening_debit": "0.00",
|
||||
"opening_credit": "1000.00",
|
||||
"turnover_debit": "500.00",
|
||||
"turnover_credit": "200.50",
|
||||
"closing_debit": "0.00",
|
||||
"closing_credit": "700.50"
|
||||
},
|
||||
{
|
||||
"row_number": 2,
|
||||
"level": 1,
|
||||
"parent_row_number": 1,
|
||||
"row_value": "ООО Лютик, ИНН 7711111111",
|
||||
"group_type": "COUNTERPARTY",
|
||||
"group_name": "Контрагенты",
|
||||
"account_code_raw": "60",
|
||||
"counterparty_name": "ООО Лютик",
|
||||
"counterparty_inn": "7711111111",
|
||||
"data_type": "BU",
|
||||
"opening_debit": "0.00",
|
||||
"opening_credit": "1000.00",
|
||||
"turnover_debit": "500.00",
|
||||
"turnover_credit": "200.50",
|
||||
"closing_debit": "0.00",
|
||||
"closing_credit": "700.50"
|
||||
},
|
||||
{
|
||||
"row_number": 3,
|
||||
"level": 1,
|
||||
"parent_row_number": 2,
|
||||
"row_value": "ООО Одуванчик, ИНН 7722222222",
|
||||
"group_type": "COUNTERPARTY",
|
||||
"group_name": "Контрагенты",
|
||||
"account_code_raw": "60",
|
||||
"counterparty_name": "ООО Одуванчик",
|
||||
"counterparty_inn": "7722222222",
|
||||
"data_type": "NU",
|
||||
"opening_debit": "0.00",
|
||||
"opening_credit": "400.00",
|
||||
"turnover_debit": "100.00",
|
||||
"turnover_credit": "50.00",
|
||||
"closing_debit": "0.00",
|
||||
"closing_credit": "400.00",
|
||||
"extra": { "КПП": "352801001" }
|
||||
}
|
||||
],
|
||||
"checks": {
|
||||
"total_row_matches_root": true,
|
||||
"balance_identity_violations": [3],
|
||||
"parent_sum_violations": []
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,13 +1,13 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "arm-ka/recognition-result/v1",
|
||||
"title": "Результат распознавания файла (tabular-parser / scan-gateway), контракт v1.0",
|
||||
"title": "Результат распознавания файла (tabular-parser / scan-gateway), контракт v1.1",
|
||||
"description": "Один файл (stored_file) → один job → N документов. Сервисы не знают БД: только структура файла. Нормализация счетов, резолв контрагентов, проверки — в arm-core.",
|
||||
"$comment": "Версионирование: schema_version = MAJOR.MINOR. MINOR (1.1) — необязательные поля и новые значения enum; MAJOR (2.0) — удаление/переименование полей, смена типов или семантики. Потребитель принимает тот же MAJOR и любой MINOR. Новый MINOR раскатывается сначала на потребителей (ядро и сервисы обновляют копии схем раньше, чем производитель начнёт слать 1.1), потому что закрытые enum отвергают незнакомые значения.",
|
||||
"type": "object",
|
||||
"required": ["schema_version", "job_id", "file_id", "service", "status", "documents"],
|
||||
"properties": {
|
||||
"schema_version": { "type": "string", "pattern": "^1\\.\\d+$", "description": "MAJOR.MINOR; производитель контракта v1.0 пишет \"1.0\"" },
|
||||
"schema_version": { "type": "string", "pattern": "^1\\.\\d+$", "description": "MAJOR.MINOR; производитель контракта v1.1 пишет \"1.1\"" },
|
||||
"job_id": { "type": "string", "format": "uuid" },
|
||||
"file_id": { "type": "string", "format": "uuid" },
|
||||
"service": {
|
||||
@ -114,7 +114,8 @@
|
||||
"turnover_debit": { "$ref": "#/$defs/money" },
|
||||
"turnover_credit": { "$ref": "#/$defs/money" },
|
||||
"closing_debit": { "$ref": "#/$defs/money" },
|
||||
"closing_credit": { "$ref": "#/$defs/money" }
|
||||
"closing_credit": { "$ref": "#/$defs/money" },
|
||||
"extra": { "type": ["object", "null"], "description": "нераспознанные колонки внутри таблицы: заголовок колонки → текст ячейки (§1.2)", "additionalProperties": { "type": ["string", "null"] } }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""Контракт обмена с сервисами распознавания v1.0 (схемы — `api/contracts/`, копии `docs/contracts/`).
|
||||
"""Контракт обмена с сервисами распознавания v1.1 (схемы — `api/contracts/`, копии `docs/contracts/`).
|
||||
|
||||
Публичный API: pydantic-модели запроса, конверта и файла результата, subject'ы NATS, идентификаторы сообщений.
|
||||
"""
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""Общие основы моделей контракта v1.0: базовый класс, версия, типы UUID / деньги / время, метрики и ошибки.
|
||||
"""Общие основы моделей контракта v1.1: базовый класс, версия, типы UUID / деньги / время, метрики и ошибки.
|
||||
|
||||
Модели повторяют JSON Schema (`api/contracts/`) буквально: схемы не содержат `additionalProperties: false`
|
||||
(кроме значений `columns` ОСВ), поэтому незнакомые поля принимаются и отбрасываются (`extra="ignore"`) —
|
||||
@ -27,7 +27,7 @@ from pydantic import (
|
||||
model_validator,
|
||||
)
|
||||
|
||||
CONTRACT_VERSION = "1.0"
|
||||
CONTRACT_VERSION = "1.1"
|
||||
|
||||
# Шаблоны версий — буквально из схем
|
||||
SCHEMA_VERSION_PATTERN = r"^1\.\d+$" # свой MAJOR, любой MINOR
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""Файл результата в S3 (`results/{file_id}/{job_id}/result.json`): `recognition-result.schema.json` v1.0.
|
||||
"""Файл результата в S3 (`results/{file_id}/{job_id}/result.json`): `recognition-result.schema.json` v1.1.
|
||||
|
||||
tabular-parser формирует этот файл по запросу; один файл → один job → N документов (ОСВ по счёту / ББ / ОФР / прочее).
|
||||
Модель намеренно совпадает с копией в arm-core (контракт — `../docs/contracts`, обзор —
|
||||
@ -102,6 +102,7 @@ class OsvRow(ContractModel):
|
||||
turnover_credit: Money = None
|
||||
closing_debit: Money = None
|
||||
closing_credit: Money = None
|
||||
extra: dict[str, str | None] | None = None # нераспознанные колонки таблицы: заголовок → текст ячейки (§1.2)
|
||||
|
||||
|
||||
class OsvChecks(ContractModel):
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""Контракт обмена с сервисами распознавания v1.0.
|
||||
"""Контракт обмена с сервисами распознавания v1.1.
|
||||
|
||||
Спека сервиса — `tabular-recognition-service`; change `recognition-contract` лежит в `../docs`.
|
||||
Быстрые тесты без БД: вендоренные схемы `api/contracts/` идентичны `../docs/contracts/`; каждый пример проходит и
|
||||
@ -56,9 +56,10 @@ EXAMPLE_KIND = {
|
||||
"result-message-processing.json": "message",
|
||||
"result-error.json": "result",
|
||||
"result-osv-minimal.json": "result",
|
||||
"result-osv-needs-check.json": "result",
|
||||
"result-statement-minimal.json": "result",
|
||||
}
|
||||
EXAMPLES_COUNT = 8
|
||||
EXAMPLES_COUNT = 9
|
||||
|
||||
|
||||
def _load(path: Path) -> Any:
|
||||
@ -453,6 +454,21 @@ CASES: list[tuple[str, str, Callable[[dict[str, Any]], None], bool]] = [
|
||||
False,
|
||||
),
|
||||
("result/osv-extra-field", "result-osv-minimal.json", _set("documents.0.debug", {"a": 1}), True),
|
||||
(
|
||||
"result/osv-row-extra-strings",
|
||||
"result-osv-minimal.json",
|
||||
_set("documents.0.rows.0.extra", {"КПП": "1", "Н": None}),
|
||||
True,
|
||||
),
|
||||
("result/osv-row-extra-null", "result-osv-minimal.json", _set("documents.0.rows.0.extra", None), True),
|
||||
("result/osv-row-extra-absent", "result-osv-minimal.json", _drop("documents.0.rows.1.extra"), True),
|
||||
(
|
||||
"result/osv-row-extra-non-string-value",
|
||||
"result-osv-minimal.json",
|
||||
_set("documents.0.rows.0.extra", {"КПП": 1}),
|
||||
False,
|
||||
),
|
||||
("result/osv-row-extra-not-object", "result-osv-minimal.json", _set("documents.0.rows.0.extra", ["КПП"]), False),
|
||||
("result/statement-no-lines", "result-statement-minimal.json", _drop("documents.0.lines"), False),
|
||||
("result/statement-no-columns", "result-statement-minimal.json", _drop("documents.0.columns"), False),
|
||||
(
|
||||
@ -588,7 +604,7 @@ def test_message_status_enum_values() -> None:
|
||||
|
||||
|
||||
def test_contract_version_constant() -> None:
|
||||
assert CONTRACT_VERSION == "1.0"
|
||||
assert CONTRACT_VERSION == "1.1"
|
||||
|
||||
|
||||
def test_python_float_amount_is_rejected_with_clear_error() -> None:
|
||||
|
||||
@ -231,7 +231,7 @@ async def test_ack_is_published_before_s3_read_and_result(env: Env) -> None:
|
||||
assert subjects == {RESULT_SUBJECT}
|
||||
ack = env.bodies()[0]
|
||||
assert ack == {
|
||||
"schema_version": "1.0",
|
||||
"schema_version": "1.1",
|
||||
"job_id": JOB_ID,
|
||||
"file_id": FILE_ID,
|
||||
"status": "processing",
|
||||
@ -368,7 +368,7 @@ async def test_unsupported_major_version_gives_schema_mismatch_without_ack(env:
|
||||
result = env.bodies()[0]
|
||||
_assert_message_valid(result)
|
||||
assert result["status"] == "error"
|
||||
assert result["schema_version"] == "1.0"
|
||||
assert result["schema_version"] == "1.1"
|
||||
assert result["job_id"] == JOB_ID
|
||||
assert result["file_id"] == FILE_ID
|
||||
assert result["error"]["code"] == "SCHEMA_MISMATCH"
|
||||
@ -503,7 +503,7 @@ async def test_substituted_parser_gives_done_with_result_file() -> None:
|
||||
_assert_message_valid(envelope)
|
||||
assert envelope["status"] == "done"
|
||||
assert envelope["result_s3"] == {"bucket": BUCKET, "key": RESULT_KEY}
|
||||
assert envelope["result_schema_version"] == "1.0"
|
||||
assert envelope["result_schema_version"] == "1.1"
|
||||
assert "error" not in envelope
|
||||
assert envelope["metrics"]["rows"] == PARSER_ROWS
|
||||
assert envelope["metrics"]["duration_ms"] >= 0
|
||||
|
||||
@ -57,9 +57,9 @@
|
||||
- `CONTRACT_VERSION = "1.1"` — воркер пишет его в `schema_version` файла и `result_schema_version` конверта (уже так).
|
||||
- Пример `result-osv-needs-check.json`: статус `needs_check`, один документ, `checks.balance_identity_violations = [3]`, `metrics.warnings = ["hierarchy=outline", "balance identity violated at rows [3]"]`, `schema_version "1.1"`, у строки 3 `extra: {"КПП": "352801001"}`.
|
||||
|
||||
- [ ] **Step 1 (docs):** внести правки схемы и примеров; проверить `python -c "import json,jsonschema; ..."` — оба примера `result-osv-*.json` валидны по новой схеме, старый `result-osv-minimal.json` без `extra` тоже валиден; `openspec validate slice-2a-osv-parser --strict` в docs → valid. Commit в docs: `contracts: результат v1.1 — rows[].extra (§1.2), пример needs_check`.
|
||||
- [ ] **Step 2 (здесь):** скопировать схему, добавить поле модели и версию; `pytest tests/unit/test_contract.py tests/unit/test_worker.py -v` → сначала падает на версии/идентичности, затем PASS после правок ожиданий (`"1.1"`).
|
||||
- [ ] **Step 3:** `make check` → PASS. Commit: `contract: v1.1 — OsvRow.extra, CONTRACT_VERSION 1.1, копия схемы`.
|
||||
- [x] **Step 1 (docs):** внести правки схемы и примеров; проверить `python -c "import json,jsonschema; ..."` — оба примера `result-osv-*.json` валидны по новой схеме, старый `result-osv-minimal.json` без `extra` тоже валиден; `openspec validate slice-2a-osv-parser --strict` в docs → valid. Commit в docs: `contracts: результат v1.1 — rows[].extra (§1.2), пример needs_check`.
|
||||
- [x] **Step 2 (здесь):** скопировать схему, добавить поле модели и версию; `pytest tests/unit/test_contract.py tests/unit/test_worker.py -v` → сначала падает на версии/идентичности, затем PASS после правок ожиданий (`"1.1"`).
|
||||
- [x] **Step 3:** `make check` → PASS. Commit: `contract: v1.1 — OsvRow.extra, CONTRACT_VERSION 1.1, копия схемы`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
`tests/fixtures_helpers.py` (`ARM_KA_FIXTURES`, как в arm-core) и `conftest.py` с фикстурой `osv_files` (все файлы
|
||||
`../fixtures/osv/**` кроме `synthetic`, параметризация по относительному пути); маркер `fixtures` в деле; `test_layout`
|
||||
не ломается. Проверка: `pytest -m fixtures --collect-only` показывает 66 параметров.
|
||||
- [ ] 1.2 Контракт v1.1: скопировать `../docs/contracts/recognition-result.schema.json` v1.1 в `api/contracts/`, `OsvRow.extra:
|
||||
- [x] 1.2 Контракт v1.1: скопировать `../docs/contracts/recognition-result.schema.json` v1.1 в `api/contracts/`, `OsvRow.extra:
|
||||
dict[str, str | None] | None`, `CONTRACT_VERSION = "1.1"`; примеры docs проходят; тест идентичности зелёный; воркер пишет
|
||||
`result_schema_version = "1.1"` (`test_worker` обновлён). Проверка: `make check`.
|
||||
- [ ] 1.3 `grid.py` и читатели: `Cell`, `Sheet`, `MergedRange`, `column_letter`; `readers/xlsx.py` (openpyxl, полное чтение,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user