All checks were successful
Deploy Trainer_LLM to llm-node / deploy (push) Successful in 2s
535 lines
22 KiB
Python
535 lines
22 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
wiki_router.py – v1.4.3 (Swagger + robustes .env + optionaler ENV-Login)
|
||
|
||
Änderungen ggü. v1.4.2:
|
||
- **/login/env** hinzugefügt: Login mit WIKI_BOT_USER/WIKI_BOT_PASSWORD aus ENV (Secrets werden nie ausgegeben)
|
||
- .env-Bootstrap robuster und **vor** dem ersten Aufruf geloggt
|
||
- /.meta/env/runtime um Credentials-Flags ergänzt (ohne Klartext)
|
||
- response_description-Strings mit JSON-Beispielen sauber gequotet
|
||
- Keine Breaking-Changes (Signaturen & Pfade unverändert)
|
||
|
||
Prefix-Hinweis:
|
||
- Der Router setzt `prefix="/import/wiki"`. In `llm_api.py` **ohne** weiteren Prefix einbinden.
|
||
"""
|
||
|
||
from typing import Dict, Any, Optional, List
|
||
from fastapi import APIRouter, HTTPException, Query
|
||
from pydantic import BaseModel, Field
|
||
from textwrap import dedent
|
||
import os, time, logging
|
||
import requests
|
||
from dotenv import load_dotenv, find_dotenv
|
||
from starlette.responses import PlainTextResponse
|
||
|
||
# -------------------------------------------------
|
||
# Logging **vor** .env-Bootstrap initialisieren
|
||
# -------------------------------------------------
|
||
logger = logging.getLogger("wiki_router")
|
||
logger.setLevel(logging.INFO)
|
||
|
||
# -------------------------------------------------
|
||
# Robustes .env-Loading (findet Datei auch außerhalb des CWD)
|
||
# -------------------------------------------------
|
||
|
||
def _bootstrap_env() -> Optional[str]:
|
||
"""Versucht mehrere typische Pfade für .env zu laden und loggt die Fundstelle.
|
||
Reihenfolge:
|
||
1) env `LLMAPI_ENV_FILE`
|
||
2) find_dotenv() relativ zum CWD
|
||
3) CWD/.env
|
||
4) Verzeichnis dieser Datei /.env
|
||
5) $HOME/.env
|
||
6) $HOME/.llm-api.env
|
||
7) /etc/llm-api.env
|
||
"""
|
||
candidates: List[str] = []
|
||
if os.getenv("LLMAPI_ENV_FILE"):
|
||
candidates.append(os.getenv("LLMAPI_ENV_FILE") or "")
|
||
fd = find_dotenv(".env", usecwd=True)
|
||
if fd:
|
||
candidates.append(fd)
|
||
candidates += [
|
||
os.path.join(os.getcwd(), ".env"),
|
||
os.path.join(os.path.dirname(__file__), ".env"),
|
||
os.path.expanduser("~/.env"),
|
||
os.path.expanduser("~/.llm-api.env"),
|
||
"/etc/llm-api.env",
|
||
]
|
||
for path in candidates:
|
||
try:
|
||
if path and os.path.exists(path):
|
||
loaded = load_dotenv(path, override=False)
|
||
if loaded:
|
||
logger.info("wiki_router: .env geladen aus %s", path)
|
||
return path
|
||
except Exception as e:
|
||
logger.warning("wiki_router: .env laden fehlgeschlagen (%s): %s", path, e)
|
||
logger.info("wiki_router: keine .env gefunden – verwende Prozess-Umgebung")
|
||
return None
|
||
|
||
_BOOTSTRAP_ENV = _bootstrap_env()
|
||
|
||
# -------------------------------------------------
|
||
# Router & Konfiguration
|
||
# -------------------------------------------------
|
||
router = APIRouter(prefix="/import/wiki", tags=["wiki"])
|
||
|
||
# Hinweis: Werte werden NACH dem .env-Bootstrap aus os.environ gelesen.
|
||
# Änderungen an .env erfordern i. d. R. einen Neustart des Dienstes.
|
||
WIKI_API_URL = os.getenv("WIKI_API_URL", "https://karatetrainer.net/api.php")
|
||
WIKI_TIMEOUT = float(os.getenv("WIKI_TIMEOUT", "15"))
|
||
WIKI_BATCH = int(os.getenv("WIKI_BATCH", "50"))
|
||
WIKI_RETRIES = int(os.getenv("WIKI_RETRIES", "1")) # zusätzliche Versuche bei Upstream-Fehlern
|
||
WIKI_SLEEPMS = int(os.getenv("WIKI_SLEEP_MS", "0")) # Throttle zwischen Requests (Millisekunden)
|
||
|
||
# Single Session (Cookies für Login)
|
||
wiki_session = requests.Session()
|
||
wiki_session.headers.update({"User-Agent": "local-llm-wiki-proxy/1.4.3"})
|
||
|
||
# -------------------------------------------------
|
||
# Schemas
|
||
# -------------------------------------------------
|
||
class WikiLoginRequest(BaseModel):
|
||
username: str = Field(..., description="MediaWiki-Benutzername (kein .env-Wert)")
|
||
password: str = Field(..., description="MediaWiki-Passwort (kein .env-Wert)")
|
||
model_config = {"json_schema_extra": {"example": {"username": "Bot", "password": "••••••"}}}
|
||
|
||
class WikiLoginResponse(BaseModel):
|
||
status: str = Field(..., description="'success' bei erfolgreichem Login")
|
||
message: Optional[str] = Field(None, description="Optionale Zusatzinfos")
|
||
|
||
class PageInfoResponse(BaseModel):
|
||
pageid: int = Field(..., description="Eindeutige PageID der MediaWiki-Seite")
|
||
title: str = Field(..., description="Aufgelöster Titel (kann von Eingabe abweichen, z. B. Redirect/Normalize)")
|
||
fullurl: str = Field(..., description="Kanonsiche URL zur Seite")
|
||
model_config = {"json_schema_extra": {"example": {"pageid": 218, "title": "Affenklatschen", "fullurl": "https://…/index.php?title=Affenklatschen"}}}
|
||
|
||
class PageContentResponse(BaseModel):
|
||
pageid: int = Field(..., description="PageID der angefragten Seite")
|
||
title: str = Field(..., description="Echo des mitgegebenen Titels (optional)")
|
||
wikitext: str = Field(..., description="Roh-Wikitext (inkl. Templates), keine Sanitization")
|
||
model_config = {"json_schema_extra": {"example": {"pageid": 218, "title": "Affenklatschen", "wikitext": "{{ÜbungInfoBox|…}}"}}}
|
||
|
||
# -------------------------------------------------
|
||
# Utils
|
||
# -------------------------------------------------
|
||
|
||
def _sleep():
|
||
if WIKI_SLEEPMS > 0:
|
||
time.sleep(WIKI_SLEEPMS / 1000.0)
|
||
|
||
|
||
def _request_with_retry(method: str, params: Dict[str, Any], *, data: Dict[str, Any] | None = None) -> requests.Response:
|
||
"""Wrapper um requests.* mit Retry/Throttle und konsistenten 502-Fehlern bei Upstream-Problemen.
|
||
Nutzt .env: WIKI_RETRIES, WIKI_SLEEP_MS, WIKI_TIMEOUT, WIKI_API_URL
|
||
"""
|
||
last_exc: Optional[Exception] = None
|
||
for attempt in range(WIKI_RETRIES + 1):
|
||
try:
|
||
if method == "GET":
|
||
resp = wiki_session.get(WIKI_API_URL, params=params, timeout=WIKI_TIMEOUT)
|
||
else:
|
||
resp = wiki_session.post(WIKI_API_URL, data=data or params, timeout=WIKI_TIMEOUT)
|
||
resp.raise_for_status()
|
||
return resp
|
||
except Exception as e:
|
||
last_exc = e
|
||
logger.warning("Upstream error on %s (try %d/%d): %s", method, attempt + 1, WIKI_RETRIES + 1, e)
|
||
_sleep()
|
||
# alle Versuche erschöpft
|
||
raise HTTPException(status_code=502, detail=f"Upstream error: {last_exc}")
|
||
|
||
|
||
def _normalize_variants(title: str) -> List[str]:
|
||
"""Erzeuge robuste Titel-Varianten: Leerzeichen/Unterstrich, Bindestrich/Dash-Varianten."""
|
||
t = (title or "").strip()
|
||
variants = {t}
|
||
if " " in t:
|
||
variants.add(t.replace(" ", "_"))
|
||
# Bindestrich / Gedankenstrich Varianten
|
||
for a, b in [("-", "–"), ("-", "—"), ("–", "-"), ("—", "-")]:
|
||
if a in t:
|
||
variants.add(t.replace(a, b))
|
||
return list(variants)
|
||
|
||
|
||
def _fetch_pageinfo_batch(titles: List[str]) -> Dict[str, Dict[str, Any]]:
|
||
"""Batch-Resolver für PageInfo (pageid, fullurl). Respektiert Redirects & Titel-Normalisierung.
|
||
Achtung: Große Kategorien werden in Chunks à WIKI_BATCH verarbeitet. Throttling via WIKI_SLEEP_MS.
|
||
"""
|
||
if not titles:
|
||
return {}
|
||
out: Dict[str, Dict[str, Any]] = {}
|
||
for i in range(0, len(titles), max(1, WIKI_BATCH)):
|
||
chunk = titles[i:i + max(1, WIKI_BATCH)]
|
||
params = {
|
||
"action": "query",
|
||
"format": "json",
|
||
"prop": "info",
|
||
"inprop": "url",
|
||
"redirects": 1,
|
||
"converttitles": 1,
|
||
"titles": "|".join(chunk),
|
||
}
|
||
resp = _request_with_retry("GET", params)
|
||
data = resp.json() or {}
|
||
q = data.get("query", {})
|
||
redirects = {d.get("from"): d.get("to") for d in (q.get("redirects") or [])}
|
||
pages = q.get("pages", {}) or {}
|
||
for pid_str, page in pages.items():
|
||
if page.get("missing") is not None or str(pid_str) == "-1":
|
||
continue
|
||
try:
|
||
pid = int(pid_str)
|
||
except ValueError:
|
||
pid = int(page.get("pageid", -1))
|
||
title_out = page.get("title")
|
||
fullurl = page.get("fullurl") or page.get("canonicalurl") or ""
|
||
if not title_out:
|
||
continue
|
||
out[title_out] = {"pageid": pid, "fullurl": fullurl}
|
||
# auch Originaltitel der Redirects auflösen
|
||
for frm, to in redirects.items():
|
||
if to == title_out and frm not in out:
|
||
out[frm] = {"pageid": pid, "fullurl": fullurl}
|
||
_sleep()
|
||
return out
|
||
|
||
# -------------------------------------------------
|
||
# Doku-Konstanten (Markdown/.env)
|
||
# -------------------------------------------------
|
||
MANUAL_WIKI_IMPORTER = dedent("""
|
||
# wiki_importer.py – Kurzanleitung
|
||
|
||
## Voraussetzungen
|
||
- API erreichbar: `GET /import/wiki/health` (Status `ok`)
|
||
- .env:
|
||
- `API_BASE_URL=http://localhost:8000`
|
||
- `WIKI_BOT_USER`, `WIKI_BOT_PASSWORD`
|
||
- optional: `EXERCISE_COLLECTION=exercises`
|
||
|
||
## Smoke-Test (3 Läufe)
|
||
```bash
|
||
python3 wiki_importer.py --title "Affenklatschen" --category "Übungen" --smoke-test
|
||
```
|
||
|
||
## Vollimport
|
||
```bash
|
||
python3 wiki_importer.py --all
|
||
# optional:
|
||
python3 wiki_importer.py --all --category "Übungen"
|
||
python3 wiki_importer.py --all --dry-run
|
||
```
|
||
|
||
## Idempotenz-Logik
|
||
- external_id = `mw:{pageid}`
|
||
- Fingerprint (sha256) über: `title, summary, execution, notes, duration_minutes, capabilities, keywords`
|
||
- Entscheid:
|
||
- not found → create
|
||
- fingerprint gleich → skip
|
||
- fingerprint ungleich → update (+ `imported_at`)
|
||
|
||
## Mapping (Wiki → Exercise)
|
||
- Schlüsselworte → `keywords` (`,`-getrennt, getrimmt, dedupliziert)
|
||
- Hilfsmittel → `equipment`
|
||
- Disziplin → `discipline`
|
||
- Durchführung/Notizen/Vorbereitung/Methodik → `execution`, `notes`, `preparation`, `method`
|
||
- Capabilities → `capabilities` (Level 1..5) + Facetten (`capability_ge1..5`, `capability_eq1..5`, `capability_keys`)
|
||
- Metadaten → `external_id`, `source="mediawiki"`, `imported_at`
|
||
|
||
## Troubleshooting
|
||
- 404 bei `/import/wiki/info?...`: prüfe Prefix (kein Doppelprefix), Titelvarianten
|
||
- 401 Login: echte User-Creds verwenden
|
||
- 502 Upstream: `WIKI_API_URL`/TLS prüfen; Timeouts/Retry/Throttle (`WIKI_TIMEOUT`, `WIKI_RETRIES`, `WIKI_SLEEP_MS`)
|
||
""")
|
||
|
||
ENV_DOC = [
|
||
{"name": "WIKI_API_URL", "desc": "Basis-URL zur MediaWiki-API (z. B. http://…/w/api.php)"},
|
||
{"name": "WIKI_TIMEOUT", "desc": "Timeout in Sekunden (Default 15)"},
|
||
{"name": "WIKI_RETRIES", "desc": "Anzahl zusätzlicher Versuche (Default 1)"},
|
||
{"name": "WIKI_SLEEP_MS", "desc": "Throttle zwischen Requests in Millisekunden (Default 0)"},
|
||
{"name": "WIKI_BATCH", "desc": "Batchgröße für Titel-Enrichment (Default 50)"},
|
||
{"name": "WIKI_BOT_USER", "desc": "(optional) Benutzername für /login/env – **Wert wird nie im Klartext zurückgegeben**"},
|
||
{"name": "WIKI_BOT_PASSWORD", "desc": "(optional) Passwort für /login/env – **Wert wird nie im Klartext zurückgegeben**"},
|
||
]
|
||
|
||
# -------------------------------------------------
|
||
# Doku-/Meta-Endpunkte
|
||
# -------------------------------------------------
|
||
@router.get(
|
||
"/manual/wiki_importer",
|
||
summary="Handbuch: wiki_importer.py (Markdown)",
|
||
description="Kompaktes Handbuch mit .env-Hinweisen, Aufrufen, Idempotenz und Troubleshooting.",
|
||
response_class=PlainTextResponse,
|
||
response_description="Markdown-Text.",
|
||
openapi_extra={
|
||
"x-codeSamples": [
|
||
{"lang": "bash", "label": "Vollimport (Standard)", "source": "python3 wiki_importer.py --all"},
|
||
{"lang": "bash", "label": "Dry-Run + Kategorie", "source": "python3 wiki_importer.py --all --category \"Übungen\" --dry-run"},
|
||
]
|
||
},
|
||
)
|
||
def manual_wiki_importer():
|
||
return MANUAL_WIKI_IMPORTER
|
||
|
||
|
||
@router.get(
|
||
"/meta/env",
|
||
summary=".env Referenz (Wiki-bezogen)",
|
||
description="Listet die relevanten Umgebungsvariablen für die Wiki-Integration auf (ohne Werte).",
|
||
response_description="Array aus {name, desc}.",
|
||
)
|
||
def meta_env() -> List[Dict[str, str]]:
|
||
return ENV_DOC
|
||
|
||
|
||
@router.get(
|
||
"/meta/env/runtime",
|
||
summary=".env Runtime (wirksame Werte)",
|
||
description="Zeigt die aktuell wirksamen Konfigurationswerte für den Wiki-Router (ohne Secrets) und die geladene .env-Quelle.",
|
||
response_description="Objekt mit 'loaded_from' und 'env' (Key→Value).",
|
||
)
|
||
def meta_env_runtime() -> Dict[str, Any]:
|
||
keys = ["WIKI_API_URL", "WIKI_TIMEOUT", "WIKI_RETRIES", "WIKI_SLEEP_MS", "WIKI_BATCH"]
|
||
has_user = bool(os.getenv("WIKI_BOT_USER"))
|
||
has_pwd = bool(os.getenv("WIKI_BOT_PASSWORD"))
|
||
return {
|
||
"loaded_from": _BOOTSTRAP_ENV,
|
||
"env": {k: os.getenv(k) for k in keys},
|
||
"credentials": {
|
||
"WIKI_BOT_USER_set": has_user,
|
||
"WIKI_BOT_PASSWORD_set": has_pwd,
|
||
"ready_for_login_env": has_user and has_pwd,
|
||
},
|
||
}
|
||
|
||
# -------------------------------------------------
|
||
# API-Endpunkte
|
||
# -------------------------------------------------
|
||
@router.get(
|
||
"/health",
|
||
summary="Ping & Site-Info des MediaWiki-Upstreams",
|
||
description=dedent(
|
||
"""
|
||
Führt einen leichten `meta=siteinfo`-Request gegen den konfigurierten MediaWiki-Upstream aus.
|
||
|
||
**Besonderheiten**
|
||
- Nutzt eine persistente `requests.Session` (Cookies werden für spätere Aufrufe wiederverwendet).
|
||
- Respektiert `.env`: `WIKI_API_URL`, `WIKI_TIMEOUT`, `WIKI_RETRIES`, `WIKI_SLEEP_MS`.
|
||
- Bei Upstream-Problemen wird **HTTP 502** geworfen (statt 500).
|
||
|
||
**Hinweis**: Je nach Wiki-Konfiguration sind detaillierte Infos (Generator/Sitename) nur **nach Login** sichtbar.
|
||
"""
|
||
),
|
||
response_description='`{"status":"ok"}` oder mit `wiki.sitename/generator` bei `verbose=1`.',
|
||
openapi_extra={
|
||
"x-codeSamples": [
|
||
{"lang": "bash", "label": "curl", "source": "curl -s 'http://localhost:8000/import/wiki/health?verbose=1' | jq ."}
|
||
]
|
||
},
|
||
)
|
||
def health(verbose: Optional[int] = Query(default=0, description="1 = Site-Metadaten (sitename/generator) mitsenden")) -> Dict[str, Any]:
|
||
resp = _request_with_retry("GET", {"action": "query", "meta": "siteinfo", "format": "json"})
|
||
if verbose:
|
||
info = resp.json().get("query", {}).get("general", {})
|
||
return {"status": "ok", "wiki": {"sitename": info.get("sitename"), "generator": info.get("generator")}}
|
||
return {"status": "ok"}
|
||
|
||
|
||
@router.post(
|
||
"/login",
|
||
response_model=WikiLoginResponse,
|
||
summary="MediaWiki-Login (Session-Cookies werden serverseitig gespeichert)",
|
||
description=dedent(
|
||
"""
|
||
Meldet den Proxy am MediaWiki an. Unterstützt `clientlogin` (mit `loginreturnurl`) und
|
||
**fällt zurück** auf `action=login`, falls erforderlich. Erfolgreiche Logins hinterlegen die
|
||
Session-Cookies in der Server-Session und gelten für nachfolgende Requests.
|
||
|
||
**Besonderheiten**
|
||
- Erwartet **Benutzername/Passwort im Body** (keine .env-Creds).
|
||
- Verwendet vor dem Login ein Logintoken (`meta=tokens`).
|
||
- Rückgabe `{\"status\":\"success\"}` bei Erfolg, sonst **401**.
|
||
- Respektiert Retry/Throttle aus `.env`.
|
||
"""
|
||
),
|
||
response_description='`{"status":"success"}` bei Erfolg.',
|
||
)
|
||
def login(data: WikiLoginRequest):
|
||
# Token holen
|
||
tok = _request_with_retry("GET", {"action": "query", "meta": "tokens", "type": "login", "format": "json"})
|
||
token = tok.json().get("query", {}).get("tokens", {}).get("logintoken")
|
||
if not token:
|
||
raise HTTPException(status_code=502, detail="Kein Login-Token erhalten")
|
||
|
||
# clientlogin (mit returnurl) + Fallback action=login
|
||
try:
|
||
cl = _request_with_retry("POST", {}, data={
|
||
"action": "clientlogin",
|
||
"format": "json",
|
||
"username": data.username,
|
||
"password": data.password,
|
||
"logintoken": token,
|
||
"loginreturnurl": "https://example.org/",
|
||
})
|
||
st = cl.json().get("clientlogin", {}).get("status")
|
||
if st == "PASS":
|
||
return WikiLoginResponse(status="success")
|
||
except HTTPException:
|
||
pass
|
||
|
||
lg = _request_with_retry("POST", {}, data={
|
||
"action": "login",
|
||
"format": "json",
|
||
"lgname": data.username,
|
||
"lgpassword": data.password,
|
||
"lgtoken": token,
|
||
})
|
||
res = lg.json().get("login", {}).get("result")
|
||
if res == "Success":
|
||
return WikiLoginResponse(status="success")
|
||
raise HTTPException(status_code=401, detail=f"Login fehlgeschlagen: {res}")
|
||
|
||
|
||
@router.post(
|
||
"/login/env",
|
||
response_model=WikiLoginResponse,
|
||
summary="MediaWiki-Login mit .env-Credentials",
|
||
description=dedent(
|
||
"""
|
||
Führt den Login mit **WIKI_BOT_USER/WIKI_BOT_PASSWORD** aus der Prozess-Umgebung durch.
|
||
Praktisch für geplante Jobs/CLI ohne Übergabe im Body. Secrets werden **nie** im Klartext zurückgegeben.
|
||
|
||
**Voraussetzung**: Beide Variablen sind gesetzt (siehe `/import/wiki/meta/env/runtime`).
|
||
"""
|
||
),
|
||
response_description='`{"status":"success"}` bei Erfolg.',
|
||
openapi_extra={
|
||
"x-codeSamples": [
|
||
{"lang": "bash", "label": "curl", "source": "curl -s -X POST http://localhost:8000/import/wiki/login/env | jq ."}
|
||
]
|
||
},
|
||
)
|
||
def login_env():
|
||
user = os.getenv("WIKI_BOT_USER")
|
||
pwd = os.getenv("WIKI_BOT_PASSWORD")
|
||
if not user or not pwd:
|
||
raise HTTPException(status_code=400, detail="WIKI_BOT_USER/WIKI_BOT_PASSWORD nicht gesetzt")
|
||
return login(WikiLoginRequest(username=user, password=pwd))
|
||
|
||
|
||
@router.get(
|
||
"/semantic/pages",
|
||
summary="SMW-Ask-Ergebnisse einer Kategorie mit PageID/URL anreichern",
|
||
description=dedent(
|
||
"""
|
||
Ruft Semantic MediaWiki via `action=ask` auf und liefert ein **Dictionary**: `{"Titel": {...}}`.
|
||
Anschließend werden **alle** Titel batchweise via `prop=info` um `pageid` und `fullurl` ergänzt
|
||
(berücksichtigt Redirects & Titel-Normalisierung). Große Kategorien werden in Chunks der Größe
|
||
`WIKI_BATCH` verarbeitet. Throttling gemäß `WIKI_SLEEP_MS`.
|
||
|
||
**Rückgabe**
|
||
- Key = Seitentitel
|
||
- Value = ursprüngliche Ask-Daten **plus** `pageid` & `fullurl` (falls auflösbar)
|
||
- Kann leeres Objekt `{}` sein (z. B. wenn Login erforderlich oder Kategorie leer).
|
||
"""
|
||
),
|
||
response_description="Dictionary pro Titel; Felder `pageid/fullurl` sind evtl. nicht für alle Titel gesetzt.",
|
||
openapi_extra={
|
||
"x-codeSamples": [
|
||
{"lang": "bash", "label": "curl", "source": "curl -s 'http://localhost:8000/import/wiki/semantic/pages?category=%C3%9Cbungen' | jq . | head"}
|
||
]
|
||
},
|
||
)
|
||
def semantic_pages(category: str = Query(..., description="Kategorie-Name **ohne** 'Category:' Präfix")) -> Dict[str, Any]:
|
||
ask_query = f"[[Category:{category}]]|limit=50000"
|
||
r = _request_with_retry("GET", {"action": "ask", "query": ask_query, "format": "json"})
|
||
results = r.json().get("query", {}).get("results", {}) or {}
|
||
titles = list(results.keys())
|
||
|
||
info_map = _fetch_pageinfo_batch(titles)
|
||
|
||
enriched: Dict[str, Any] = {}
|
||
missing = 0
|
||
for title, entry in results.items():
|
||
base = entry if isinstance(entry, dict) else {}
|
||
extra = info_map.get(title, {})
|
||
if not extra:
|
||
missing += 1
|
||
enriched[title] = {
|
||
**base,
|
||
"pageid": extra.get("pageid", base.get("pageid")),
|
||
"fullurl": extra.get("fullurl", base.get("fullurl")),
|
||
}
|
||
logger.info("/semantic/pages: %d Titel, %d ohne pageid nach Enrichment", len(results), missing)
|
||
return enriched
|
||
|
||
|
||
@router.get(
|
||
"/parsepage",
|
||
response_model=PageContentResponse,
|
||
summary="Wikitext einer Seite per pageid holen",
|
||
description=dedent(
|
||
"""
|
||
Liefert den **Roh-Wikitext** (`prop=wikitext`) zu einer Seite. Der optionale `title`-Parameter dient
|
||
nur als Echo/Diagnose. Für strukturierte Extraktion (Infobox/Abschnitte) muss der Aufrufer den
|
||
Wikitext selbst parsen.
|
||
|
||
**Besonderheiten**
|
||
- Erfordert ggf. vorherigen Login (private Wikis).
|
||
- Throttling/Retry gemäß `.env`.
|
||
- Upstream-Fehler werden als **502** gemeldet.
|
||
"""
|
||
),
|
||
response_description="Roh-Wikitext (keine HTML-Transformation).",
|
||
openapi_extra={
|
||
"x-codeSamples": [
|
||
{"lang": "bash", "label": "curl", "source": "curl -s 'http://localhost:8000/import/wiki/parsepage?pageid=218&title=Affenklatschen' | jq ."}
|
||
]
|
||
},
|
||
)
|
||
def parse_page(pageid: int = Query(..., description="Numerische PageID der Seite"), title: str = Query(None, description="Optional: Seitentitel (nur Echo)")):
|
||
resp = _request_with_retry("GET", {"action": "parse", "pageid": pageid, "prop": "wikitext", "format": "json"})
|
||
wikitext = resp.json().get("parse", {}).get("wikitext", {}).get("*", "")
|
||
return PageContentResponse(pageid=pageid, title=title or "", wikitext=wikitext)
|
||
|
||
|
||
@router.get(
|
||
"/info",
|
||
response_model=PageInfoResponse,
|
||
summary="PageID/URL zu einem Titel auflösen (inkl. Varianten)",
|
||
description=dedent(
|
||
"""
|
||
Versucht zuerst eine **exakte** Auflösung des angegebenen Titels (mit `redirects=1`, `converttitles=1`).
|
||
Falls nicht erfolgreich, werden **Titel-Varianten** getestet (Leerzeichen↔Unterstrich, Bindestrich↔Gedankenstrich).
|
||
Bei Erfolg Rückgabe mit `pageid`, aufgelöstem `title` und `fullurl`. Andernfalls **404**.
|
||
|
||
**Typische Fälle**
|
||
- Unterschiedliche Schreibweisen (z. B. "Yoko Geri" vs. "Yoko_Geri").
|
||
- Redirect-Ketten → es wird der **kanonische** Titel/URL geliefert.
|
||
"""
|
||
),
|
||
response_description="Erfolg: PageInfo. Fehler: 404 'Page not found: <title>'.",
|
||
openapi_extra={
|
||
"x-codeSamples": [
|
||
{"lang": "bash", "label": "curl", "source": "curl -s 'http://localhost:8000/import/wiki/info?title=Affenklatschen' | jq ."}
|
||
]
|
||
},
|
||
)
|
||
def page_info(title: str = Query(..., description="Seitentitel (unscharf; Varianten werden versucht)")):
|
||
res = _fetch_pageinfo_batch([title])
|
||
if res.get(title):
|
||
d = res[title]
|
||
return PageInfoResponse(pageid=d["pageid"], title=title, fullurl=d.get("fullurl", ""))
|
||
|
||
for v in _normalize_variants(title):
|
||
if v == title:
|
||
continue
|
||
res2 = _fetch_pageinfo_batch([v])
|
||
if res2.get(v):
|
||
d = res2[v]
|
||
return PageInfoResponse(pageid=d["pageid"], title=v, fullurl=d.get("fullurl", ""))
|
||
|
||
raise HTTPException(status_code=404, detail=f"Page not found: {title}")
|