feat: enhance MediaWiki integration and error handling in SMW client
Some checks failed
Deploy Development / deploy (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 5s
Test Suite / playwright-tests (push) Failing after 46s

- Added MediaWiki API configuration options to docker-compose.yml for production environment.
- Improved error handling in the SMW client by introducing a response parsing method to manage API errors more effectively.
- Updated methods in the SMW client to ensure robust handling of API responses, preventing potential crashes due to unexpected data structures.
- Enhanced the import functionality to handle last imported timestamps more gracefully.
This commit is contained in:
Lars 2026-04-29 13:50:54 +02:00
parent 07b1cd8209
commit 362eb4145f
3 changed files with 71 additions and 26 deletions

View File

@ -120,7 +120,9 @@ async def preview_import(
"wiki_page_title": page_title, "wiki_page_title": page_title,
"wiki_page_id": page_id, "wiki_page_id": page_id,
"already_imported": existing_ref is not None, "already_imported": existing_ref is not None,
"last_imported_at": existing_ref["last_imported"].isoformat() if existing_ref else None, "last_imported_at": existing_ref["last_imported"].isoformat()
if existing_ref and existing_ref.get("last_imported") is not None
else None,
"mapped_fields": mapped_fields, "mapped_fields": mapped_fields,
"warnings": warnings, "warnings": warnings,
"errors": errors, "errors": errors,
@ -639,21 +641,38 @@ def _upsert_skill(mapped: dict, reimport: bool) -> Optional[int]:
return skill_id return skill_id
def _upsert_method(mapped: dict, reimport: bool) -> Optional[int]: def _upsert_method(mapped: dict, _reimport: bool) -> Optional[int]:
"""Legt Trainingsmethode an oder aktualisiert Beschreibung.""" """Legt Trainingsmethode an oder aktualisiert Beschreibung nach Namen.
training_methods.name hat keinen UNIQUE-Constraint daher kein ON CONFLICT (name).
"""
name = (mapped.get("name") or "").strip()
desc = mapped.get("description")
if not name:
return None
with get_db() as conn: with get_db() as conn:
cur = get_cursor(conn) cur = get_cursor(conn)
cur.execute( cur.execute(
"""INSERT INTO training_methods (name, description) "SELECT id FROM training_methods WHERE TRIM(name) ILIKE TRIM(%s)",
VALUES (%s, %s) (name,),
ON CONFLICT (name) DO UPDATE SET
description = EXCLUDED.description
RETURNING id""",
(mapped["name"], mapped.get("description"))
) )
method_id = cur.fetchone()['id'] row = cur.fetchone()
if row:
mid = row["id"]
cur.execute(
"UPDATE training_methods SET description = %s, updated_at = NOW() WHERE id = %s",
(desc, mid),
)
conn.commit()
return mid
cur.execute(
"INSERT INTO training_methods (name, description) VALUES (%s, %s) RETURNING id",
(name, desc),
)
mid = cur.fetchone()["id"]
conn.commit() conn.commit()
return method_id return mid
def _upsert_wiki_ref( def _upsert_wiki_ref(

View File

@ -48,7 +48,11 @@ class SmwClient:
"format": "json", "format": "json",
}) })
r1.raise_for_status() r1.raise_for_status()
token = r1.json()["query"]["tokens"]["logintoken"] j = self._parse_mw_response(r1.json())
tok = (((j.get("query") or {}).get("tokens") or {}).get("logintoken"))
if not tok:
raise SmwClientError("MediaWiki Login-Token fehlt in API-Antwort")
cookies = dict(r1.cookies) cookies = dict(r1.cookies)
# Schritt 2: Einloggen # Schritt 2: Einloggen
@ -56,10 +60,10 @@ class SmwClient:
"action": "login", "action": "login",
"lgname": self.user, "lgname": self.user,
"lgpassword": self.password, "lgpassword": self.password,
"lgtoken": token, "lgtoken": tok,
}, cookies=cookies) }, cookies=cookies)
r2.raise_for_status() r2.raise_for_status()
result = r2.json() result = self._parse_mw_response(r2.json())
if result.get("login", {}).get("result") != "Success": if result.get("login", {}).get("result") != "Success":
reason = result.get("login", {}).get("reason", "unbekannt") reason = result.get("login", {}).get("reason", "unbekannt")
@ -69,15 +73,31 @@ class SmwClient:
self._logged_in = True self._logged_in = True
logger.info("SMW Login erfolgreich als '%s'", self.user) logger.info("SMW Login erfolgreich als '%s'", self.user)
def _parse_mw_response(self, data: dict) -> dict:
"""MediaWiki liefert oft HTTP 200 mit {\"error\": {...}} — sonst KeyErrors in Client-Code."""
if not isinstance(data, dict):
raise SmwClientError("Ungültige API-Antwort (kein JSON-Objekt)")
err = data.get("error")
if isinstance(err, dict):
code = err.get("code", "")
info = err.get("info") or err.get("*") or err.get("message") or ""
raise SmwClientError(f"MediaWiki API: {code}: {info}".strip())
return data
async def _get(self, params: dict) -> dict: async def _get(self, params: dict) -> dict:
"""Authentifizierter GET-Request gegen die API.""" """Authentifizierter GET-Request gegen die API."""
if not self._logged_in: if not self._logged_in:
await self.login() await self.login()
params["format"] = "json" params["format"] = "json"
async with httpx.AsyncClient(timeout=30, cookies=self._cookies) as client: try:
r = await client.get(self.api_url, params=params) async with httpx.AsyncClient(timeout=30, cookies=self._cookies) as client:
r.raise_for_status() r = await client.get(self.api_url, params=params)
return r.json() r.raise_for_status()
return self._parse_mw_response(r.json())
except SmwClientError:
raise
except Exception as e:
raise SmwClientError(str(e))
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
# Kategorien # # Kategorien #
@ -104,7 +124,8 @@ class SmwClient:
params["cmcontinue"] = cmcontinue params["cmcontinue"] = cmcontinue
data = await self._get(params) data = await self._get(params)
members.extend(data["query"]["categorymembers"]) q = data.get("query") or {}
members.extend(q.get("categorymembers") or [])
if "continue" in data and len(members) < limit: if "continue" in data and len(members) < limit:
cmcontinue = data["continue"].get("cmcontinue") cmcontinue = data["continue"].get("cmcontinue")
@ -143,7 +164,8 @@ class SmwClient:
params["cmcontinue"] = cmcontinue params["cmcontinue"] = cmcontinue
data = await self._get(params) data = await self._get(params)
subcats.extend(data["query"]["categorymembers"]) q = data.get("query") or {}
subcats.extend(q.get("categorymembers") or [])
if "continue" in data: if "continue" in data:
cmcontinue = data["continue"].get("cmcontinue") cmcontinue = data["continue"].get("cmcontinue")
@ -193,12 +215,9 @@ class SmwClient:
"action": "browsebysubject", "action": "browsebysubject",
"subject": title, "subject": title,
}) })
if "error" in data:
raise SmwClientError(f"SMW Browse-Fehler für '{title}': {data['error']}")
# Normalisiere: {property_label: [wert1, wert2]} # Normalisiere: {property_label: [wert1, wert2]}
result = {} result = {}
for prop_data in data.get("query", {}).get("data", []): for prop_data in (data.get("query") or {}).get("data") or []:
prop_label = prop_data.get("property", "") prop_label = prop_data.get("property", "")
if prop_label.startswith("_"): # Interne SMW-Properties überspringen if prop_label.startswith("_"): # Interne SMW-Properties überspringen
continue continue
@ -223,8 +242,6 @@ class SmwClient:
"action": "ask", "action": "ask",
"query": f"{query}|limit={limit}", "query": f"{query}|limit={limit}",
}) })
if "error" in data:
raise SmwClientError(f"SMW Ask-Fehler: {data['error']}")
results = [] results = []
for title, props in data.get("query", {}).get("results", {}).items(): for title, props in data.get("query", {}).get("results", {}).items():

View File

@ -42,6 +42,15 @@ services:
APP_URL: "${APP_URL:-https://shinkan.jinkendo.de}" APP_URL: "${APP_URL:-https://shinkan.jinkendo.de}"
ALLOWED_ORIGINS: "${ALLOWED_ORIGINS:-https://shinkan.jinkendo.de}" ALLOWED_ORIGINS: "${ALLOWED_ORIGINS:-https://shinkan.jinkendo.de}"
ENVIRONMENT: "${ENVIRONMENT:-production}" ENVIRONMENT: "${ENVIRONMENT:-production}"
# MediaWiki/SMW Import — in dev-env.yml bereits gesetzt; Prod brauchte diese Zeilen ebenfalls,
# sonst: leere MEDIAWIKI_API_URL im Container → Import bricht ab (auf Test/Dev war es immer gesetzt).
MEDIAWIKI_API_URL: "${MEDIAWIKI_API_URL:-https://karatetrainer.net/api.php}"
MEDIAWIKI_USER: "${MEDIAWIKI_USER:-}"
MEDIAWIKI_PASSWORD: "${MEDIAWIKI_PASSWORD:-}"
MEDIAWIKI_CATEGORY_EXERCISES: "${MEDIAWIKI_CATEGORY_EXERCISES:-Übungen}"
MEDIAWIKI_CATEGORY_SKILLS: "${MEDIAWIKI_CATEGORY_SKILLS:-Fähigkeitsbeschreibung}"
MEDIAWIKI_CATEGORY_METHODS: "${MEDIAWIKI_CATEGORY_METHODS:-Methodenbeschreibung}"
MEDIAWIKI_CATEGORY_MODELS: "${MEDIAWIKI_CATEGORY_MODELS:-Reifegradmodelle}"
volumes: volumes:
- shinkan-media:/app/media - shinkan-media:/app/media
ports: ports: