a6a6609c48
- db/migrations/001_init.sql — схема (4 сущности, prices jsonb, pgvector/pg_trgm) - contracts/models.py — общие модели (RawRow, ParsedDocument, MatchResult, *Out) - etl/dictionary.py — загрузчик справочника (1281 услуга, нормализация) - api/main.py — FastAPI: 7 эндпоинтов ТЗ + /stats (контракт) - infra/ — docker-compose (pg+pgvector, api, web), Caddyfile - scripts/proof/phase0_proof.py — проверка каскада на реальных данных (35% без эмбеддингов) - data/ в .gitignore: данные клиник в публичный репо не коммитятся Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
70 lines
2.9 KiB
Python
70 lines
2.9 KiB
Python
"""MedArchive API — эндпоинты ТЗ §4.5 плюс /stats для дашборда.
|
|
|
|
Здесь зафиксирован контракт API (пути, параметры, модели ответа). Реализация
|
|
запросов к БД — задача агента D; пока эндпоинты возвращают пустые заготовки,
|
|
чтобы приложение поднималось и фронт мог разрабатываться на сгенерированной
|
|
OpenAPI-схеме.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from fastapi import FastAPI, Query
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
from contracts.models import PartnerOut, PriceOut, ServiceOut, Stats # noqa: E402
|
|
|
|
app = FastAPI(
|
|
title="MedArchive API",
|
|
version="0.1.0",
|
|
description="Поиск услуг и цен по архиву прайсов клиник-партнёров.",
|
|
)
|
|
|
|
|
|
@app.get("/services", response_model=list[ServiceOut], summary="Услуги справочника")
|
|
def list_services(specialty: str | None = Query(None, description="Фильтр по специальности")):
|
|
return [] # TODO(D): выборка из таблицы service
|
|
|
|
|
|
@app.get("/services/{service_id}/partners", response_model=list[PriceOut],
|
|
summary="Кто оказывает услугу и по какой цене")
|
|
def service_partners(service_id: str):
|
|
return [] # TODO(D): партнёры с ценами по услуге (ядро WOW-сравнения)
|
|
|
|
|
|
@app.get("/partners", response_model=list[PartnerOut], summary="Партнёры")
|
|
def list_partners(city: str | None = None, is_active: bool | None = None):
|
|
return [] # TODO(D)
|
|
|
|
|
|
@app.get("/partners/{partner_id}/services", response_model=list[PriceOut],
|
|
summary="Все услуги партнёра с ценами")
|
|
def partner_services(partner_id: str):
|
|
return [] # TODO(D)
|
|
|
|
|
|
@app.get("/search", summary="Полнотекстовый поиск по услугам и партнёрам")
|
|
def search(q: str = Query(..., min_length=1)):
|
|
return {"services": [], "partners": []} # TODO(D)
|
|
|
|
|
|
@app.get("/unmatched", summary="Несопоставленные позиции (для операторов)")
|
|
def unmatched(limit: int = 50, offset: int = 0):
|
|
return [] # TODO(D)
|
|
|
|
|
|
@app.post("/match", summary="Ручное сопоставление позиции со справочником")
|
|
def manual_match(item_id: str, service_id: str):
|
|
return {"item_id": item_id, "service_id": service_id, "status": "todo"} # TODO(D)
|
|
|
|
|
|
@app.get("/stats", response_model=Stats, summary="Сводка качества обработки")
|
|
def stats():
|
|
return Stats() # TODO(D): реальные метрики из БД
|
|
|
|
|
|
@app.get("/health", summary="Проверка живости")
|
|
def health():
|
|
return {"status": "ok"}
|