Files
aza/AzA march 2026/aza_bibliothek_sync.py

222 lines
9.1 KiB
Python
Raw Normal View History

2026-06-10 22:55:03 +02:00
# -*- coding: utf-8 -*-
"""Server-Sync für veröffentlichte Bibliotheks-Begriffe (lokal, kein Deploy in diesem Block)."""
from __future__ import annotations
import sqlite3
import time
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
from fastapi import Depends, HTTPException, Request
from pydantic import BaseModel, Field
from aza_security import require_api_token
MAX_TERM_CHARS = 200
def ensure_published_library_terms_schema(con: sqlite3.Connection) -> None:
con.execute(
"""
CREATE TABLE IF NOT EXISTS published_library_terms (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
practice_id TEXT NOT NULL,
category TEXT NOT NULL,
term TEXT NOT NULL,
preferred_spelling TEXT NOT NULL,
variants TEXT,
description TEXT,
brand_name TEXT,
active_substance TEXT,
language TEXT,
market_region TEXT,
revision INTEGER DEFAULT 1,
status TEXT DEFAULT 'published',
created_at INTEGER,
updated_at INTEGER
)
"""
)
con.execute(
"CREATE INDEX IF NOT EXISTS idx_pub_lib_cat_status "
"ON published_library_terms(category, status, updated_at)"
)
con.commit()
def _now_ts() -> int:
return int(time.time())
def _resolve_auth_context(con: sqlite3.Connection, request: Request) -> Tuple[str, str]:
from aza_sync_items import _resolve_practice_id_for_request
practice_id = _resolve_practice_id_for_request(con, request)
user_id = (request.headers.get("X-User-Id") or request.headers.get("X-Empfang-User-Id") or "").strip()
if not user_id:
raise HTTPException(status_code=403, detail="user_id required")
if practice_id == "default":
raise HTTPException(status_code=403, detail="invalid practice_id")
return user_id, practice_id
class PublishLibraryTermIn(BaseModel):
category: str = Field(min_length=1, max_length=40)
term: str = Field(min_length=1, max_length=MAX_TERM_CHARS)
preferred_spelling: str = Field(min_length=1, max_length=MAX_TERM_CHARS)
variants: str = Field(default="", max_length=500)
description: str = Field(default="", max_length=1000)
brand_name: str = Field(default="", max_length=120)
active_substance: str = Field(default="", max_length=120)
language: str = Field(default="de")
market_region: str = Field(default="de-CH")
server_id: Optional[str] = ""
def register_bibliothek_routes(app, *, stripe_db_path_fn) -> None:
"""Registriert /v1/library/* — lokal, Deploy separat."""
@app.get("/v1/library/public", dependencies=[Depends(require_api_token)])
def library_public_list(request: Request, category: str = ""):
db_path = stripe_db_path_fn()
if not Path(db_path).exists():
raise HTTPException(status_code=503, detail="database missing")
with sqlite3.connect(db_path) as con:
ensure_published_library_terms_schema(con)
_resolve_auth_context(con, request)
if category.strip():
rows = con.execute(
"""
SELECT id, category, term, preferred_spelling, variants, description,
brand_name, active_substance, language, market_region, revision, updated_at
FROM published_library_terms
WHERE status = 'published' AND category = ?
ORDER BY category, updated_at DESC
""",
(category.strip(),),
).fetchall()
else:
rows = con.execute(
"""
SELECT id, category, term, preferred_spelling, variants, description,
brand_name, active_substance, language, market_region, revision, updated_at
FROM published_library_terms
WHERE status = 'published'
ORDER BY category, updated_at DESC
"""
).fetchall()
items = [
{
"item_id": r[0],
"scope": "public",
"category": r[1],
"term": r[2],
"preferred_spelling": r[3],
"variants": [v for v in (r[4] or "").split("|") if v.strip()],
"description": r[5] or "",
"brand_name": r[6] or "",
"active_substance": r[7] or "",
"language": r[8] or "de",
"market_region": r[9] or "de-CH",
"revision": int(r[10] or 1),
"source": "doctor_published",
"active": True,
"status": "published",
}
for r in rows
]
return {"ok": True, "items": items, "count": len(items)}
@app.post("/v1/library/publish", dependencies=[Depends(require_api_token)])
def library_publish(request: Request, body: PublishLibraryTermIn):
from aza_bibliothek import publish_payload_sanitize
ok, reason = publish_payload_sanitize(body.model_dump())
if not ok:
raise HTTPException(status_code=400, detail=reason)
db_path = stripe_db_path_fn()
if not Path(db_path).exists():
raise HTTPException(status_code=503, detail="database missing")
with sqlite3.connect(db_path) as con:
ensure_published_library_terms_schema(con)
user_id, practice_id = _resolve_auth_context(con, request)
now = _now_ts()
pub_id = (body.server_id or "").strip() or f"lib_{user_id}_{now}"
existing = con.execute(
"SELECT user_id, revision, created_at FROM published_library_terms WHERE id = ?",
(pub_id,),
).fetchone()
if existing and existing[0] != user_id:
raise HTTPException(status_code=403, detail="not owner")
rev = int(existing[1] or 0) + 1 if existing else 1
created_at = int(existing[2] or now) if existing else now
con.execute(
"""
INSERT INTO published_library_terms
(id, user_id, practice_id, category, term, preferred_spelling, variants,
description, brand_name, active_substance, language, market_region,
revision, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'published', ?, ?)
ON CONFLICT(id) DO UPDATE SET
category=excluded.category, term=excluded.term,
preferred_spelling=excluded.preferred_spelling, variants=excluded.variants,
description=excluded.description, brand_name=excluded.brand_name,
active_substance=excluded.active_substance, language=excluded.language,
market_region=excluded.market_region, revision=excluded.revision,
status='published', updated_at=excluded.updated_at
""",
(
pub_id, user_id, practice_id, body.category.strip(),
body.term.strip(), body.preferred_spelling.strip(),
body.variants.strip(), body.description.strip(),
body.brand_name.strip(), body.active_substance.strip(),
body.language.strip(), body.market_region.strip(),
rev, created_at, now,
),
)
con.commit()
return {"ok": True, "id": pub_id, "revision": rev}
@app.post("/v1/library/unpublish/{pub_id}", dependencies=[Depends(require_api_token)])
def library_unpublish(pub_id: str, request: Request):
db_path = stripe_db_path_fn()
with sqlite3.connect(db_path) as con:
ensure_published_library_terms_schema(con)
user_id, _ = _resolve_auth_context(con, request)
row = con.execute(
"SELECT user_id FROM published_library_terms WHERE id = ?", (pub_id,),
).fetchone()
if not row:
raise HTTPException(status_code=404, detail="not found")
if row[0] != user_id:
raise HTTPException(status_code=403, detail="not owner")
con.execute(
"UPDATE published_library_terms SET status='unpublished', updated_at=? WHERE id=?",
(_now_ts(), pub_id),
)
con.commit()
return {"ok": True, "id": pub_id, "status": "unpublished"}
def fetch_public_library_from_server(app) -> List[Dict[str, Any]]:
"""Client: öffentliche Begriffe vom Server (falls erreichbar)."""
try:
from aza_doku_prompt_sync import _sync_headers
import requests
headers = _sync_headers(app)
if not headers:
return []
from openai_runtime_config import get_backend_base_url
base = (get_backend_base_url() or "").rstrip("/")
if not base:
return []
resp = requests.get(f"{base}/v1/library/public", headers=headers, timeout=12)
if resp.status_code != 200:
return []
data = resp.json()
return data.get("items") if isinstance(data, dict) else []
except Exception:
return []