diff --git a/deploy/repository.py b/deploy/repository.py new file mode 100644 index 0000000..4c77cae --- /dev/null +++ b/deploy/repository.py @@ -0,0 +1,193 @@ +"""Repository facade — Protocol поверх db/*.py. + +План decoupling Ф3: +- Repository (Protocol) — интерфейс доступа к данным +- PgRepository — реальная БД (обёртка над db/*.py) +- MemRepository — in-memory для юнит-тестов +- SQL не переписываем +""" +from typing import Protocol, Optional +from datetime import datetime +import uuid + + +# ── Протокол ──────────────────────────────────────────────────── + +class Repository(Protocol): + """Фасад доступа к данным.""" + + def insert_document(self, filename: str, mime_type: str, original_bytes: str, + batch_id: str = None, zip_source: str = None) -> dict: + """Вставить документ, вернуть row dict.""" + ... + + def set_document_parsed(self, doc_id: str, elements: list) -> None: + """Обновить elements_json + status='parsed'.""" + ... + + def set_document_error(self, doc_id: str, error: str) -> None: + """Обновить status='error'.""" + ... + + def set_classification(self, doc_id: str, doc_type: str, own_number: str = None, + parent_number: str = None, doc_date: str = None, + counterparty: str = None) -> None: + """Сохранить результат классификации.""" + ... + + def set_classify_garbage(self, doc_id: str, reason: str = "") -> None: + """Пометить документ как мусор.""" + ... + + def set_classify_failed(self, doc_id: str, error: str) -> None: + """Пометить классификацию как failed.""" + ... + + def list_pending(self, batch_id: str) -> list[dict]: + """Документы, ожидающие классификации.""" + ... + + def insert_contract(self, number: str, client: str = "") -> str: + """Создать контракт, вернуть contract_id.""" + ... + + def insert_supplement(self, contract_id: str, doc_id: str, supp_type: str) -> None: + """Создать связь contract↔document.""" + ... + + def list_supplements(self, contract_id: str) -> list[dict]: + """Список дополнений контракта.""" + ... + + def get_spec_current(self, contract_id: str) -> list[dict]: + """Текущая спецификация контракта.""" + ... + + +# ── Продакшен: обёртка над db/*.py ────────────────────────────── + +class PgRepository: + """Реальный доступ к PostgreSQL через существующие db/*.py.""" + + def insert_document(self, filename, mime_type, original_bytes, batch_id=None, zip_source=None): + from db import documents + return documents.insert(filename, mime_type, original_bytes, + batch_id=batch_id, zip_source=zip_source) + + def set_document_parsed(self, doc_id, elements): + from db import documents + import json as _json + documents.set_parsed(doc_id, _json.dumps(elements, ensure_ascii=False)) + + def set_document_error(self, doc_id, error): + from db import documents + documents.set_error(doc_id, error) + + def set_classification(self, doc_id, doc_type, own_number=None, parent_number=None, + doc_date=None, counterparty=None): + from db import documents + documents.set_classification(doc_id, doc_type, own_number, parent_number, + doc_date, counterparty) + + def set_classify_garbage(self, doc_id, reason=""): + from db import documents + documents.set_classify_garbage(doc_id, reason) + + def set_classify_failed(self, doc_id, error): + from db import documents + documents.set_classify_failed(doc_id, error) + + def list_pending(self, batch_id): + from db import documents + return documents.list_pending(batch_id) + + def insert_contract(self, number, client=""): + from db import contracts + c = contracts.insert(number, client) + return c["id"] if c else "" + + def insert_supplement(self, contract_id, doc_id, supp_type): + from db import supplements + supplements.insert(contract_id, doc_id, supp_type) + + def list_supplements(self, contract_id): + from db import supplements + return supplements.list_by_contract(contract_id) + + def get_spec_current(self, contract_id): + from db import spec_current + return spec_current.list_by_contract(contract_id) + + +# ── Тестовый: in-memory заглушка ──────────────────────────────── + +class MemRepository: + """In-memory хранилище для юнит-тестов.""" + + def __init__(self): + self.documents: dict[str, dict] = {} + self.contracts: dict[str, dict] = {} + self.supplements: list[dict] = [] + self.spec_current: dict[str, list[dict]] = {} + + def insert_document(self, filename, mime_type, original_bytes, batch_id=None, zip_source=None): + doc_id = str(uuid.uuid4()) + self.documents[doc_id] = { + "id": doc_id, "filename": filename, "mime_type": mime_type, + "original_bytes": original_bytes, "status": "uploaded", + "elements_json": None, "doc_type": None, "own_number": None, + "parent_number": None, "doc_date": None, "counterparty": None, + "classify_status": "pending", "batch_id": batch_id, "zip_source": zip_source, + } + return self.documents[doc_id] + + def set_document_parsed(self, doc_id, elements): + if doc_id in self.documents: + self.documents[doc_id]["elements_json"] = elements + self.documents[doc_id]["status"] = "parsed" + + def set_document_error(self, doc_id, error): + if doc_id in self.documents: + self.documents[doc_id]["status"] = "error" + self.documents[doc_id]["error_message"] = error + + def set_classification(self, doc_id, doc_type, own_number=None, parent_number=None, + doc_date=None, counterparty=None): + if doc_id in self.documents: + d = self.documents[doc_id] + d["doc_type"] = doc_type + d["own_number"] = own_number + d["parent_number"] = parent_number + d["doc_date"] = doc_date + d["counterparty"] = counterparty + d["classify_status"] = "classified" + + def set_classify_garbage(self, doc_id, reason=""): + if doc_id in self.documents: + self.documents[doc_id]["doc_type"] = "garbage" + self.documents[doc_id]["classify_status"] = "garbage" + + def set_classify_failed(self, doc_id, error): + if doc_id in self.documents: + self.documents[doc_id]["classify_status"] = "failed" + self.documents[doc_id]["error_message"] = error + + def list_pending(self, batch_id): + return [d for d in self.documents.values() + if d.get("batch_id") == batch_id and d.get("classify_status") == "pending"] + + def insert_contract(self, number, client=""): + cid = str(uuid.uuid4()) + self.contracts[cid] = {"id": cid, "number": number, "client": client} + return cid + + def insert_supplement(self, contract_id, doc_id, supp_type): + self.supplements.append({ + "contract_id": contract_id, "document_id": doc_id, "type": supp_type, + }) + + def list_supplements(self, contract_id): + return [s for s in self.supplements if s["contract_id"] == contract_id] + + def get_spec_current(self, contract_id): + return self.spec_current.get(contract_id, [])