"""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, classify_raw: str = None, classify_input: 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]: """Текущая спецификация контракта.""" ... def get_document(self, doc_id: str) -> dict | None: """Получить документ по id.""" ... def list_by_batch(self, batch_id: str) -> list[dict]: """Все документы батча с полями классификации.""" ... def count_by_status(self, batch_id: str) -> dict[str, int]: """Количество документов по classify_status.""" ... def reset_classify_status(self, batch_id: str) -> None: """Сбросить classify_status на 'pending'.""" ... def set_classify_processing(self, doc_id: str) -> None: """Пометить документ как обрабатываемый.""" ... def delete_document(self, doc_id: str) -> None: """Удалить документ.""" ... # ── Продакшен: обёртка над db/*.py ────────────────────────────── class PgRepository: """Реальный доступ к PostgreSQL через существующие db/*.py.""" def insert_document(self, filename, mime_type, original_bytes, batch_id=None, zip_source=None): from app.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 app.db import documents documents.set_parsed(doc_id, elements) # documents.set_parsed уже делает json.dumps def set_document_error(self, doc_id, error): from app.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, classify_raw=None, classify_input=None): from app.db import documents documents.set_classification(doc_id, doc_type, own_number, parent_number, doc_date, counterparty, classify_raw=classify_raw, classify_input=classify_input) def set_classify_garbage(self, doc_id, reason=""): from app.db import documents documents.set_classify_garbage(doc_id, reason) def set_classify_failed(self, doc_id, error): from app.db import documents documents.set_classify_failed(doc_id, error) def list_pending(self, batch_id): from app.db import documents return documents.list_pending(batch_id) def insert_contract(self, number, client=""): from app.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 app.db import supplements supplements.insert(contract_id, doc_id, supp_type) def list_supplements(self, contract_id): from app.db import supplements return supplements.list_by_contract(contract_id) def get_spec_current(self, contract_id): from app.db import spec_current return spec_current.list_by_contract(contract_id) def get_document(self, doc_id): from app.db import documents return documents.get(doc_id) def list_by_batch(self, batch_id): from app.db import documents return documents.list_by_batch(batch_id) def count_by_status(self, batch_id): from app.db import documents return documents.count_by_status(batch_id) def reset_classify_status(self, batch_id): from app.db import documents documents.reset_classify_status(batch_id) def set_classify_processing(self, doc_id): from app.db import documents documents.set_classify_processing(doc_id) def delete_document(self, doc_id): from app.db import documents documents.delete(doc_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, classify_raw=None, classify_input=None): if doc_id in self.documents: d = self.documents[doc_id] d.update({"doc_type": doc_type, "own_number": own_number, "parent_number": parent_number, "doc_date": doc_date, "counterparty": counterparty, "classify_raw": classify_raw, "classify_input": classify_input, "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, []) def get_document(self, doc_id): return self.documents.get(doc_id) def list_by_batch(self, batch_id): return [d for d in self.documents.values() if d.get("batch_id") == batch_id] def count_by_status(self, batch_id): counts = {} for d in self.documents.values(): if d.get("batch_id") == batch_id: s = d.get("classify_status", "unknown") counts[s] = counts.get(s, 0) + 1 return counts def reset_classify_status(self, batch_id): for d in self.documents.values(): if d.get("batch_id") == batch_id: d["classify_status"] = "pending" def set_classify_processing(self, doc_id): if doc_id in self.documents: self.documents[doc_id]["classify_status"] = "processing" def delete_document(self, doc_id): self.documents.pop(doc_id, None)