222 lines
9.1 KiB
Python
222 lines
9.1 KiB
Python
# -*- 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 []
|