From 362eb4145f8f0f5ebb6d686ecb9b56a6c6d164c6 Mon Sep 17 00:00:00 2001 From: Lars Date: Wed, 29 Apr 2026 13:50:54 +0200 Subject: [PATCH] feat: enhance MediaWiki integration and error handling in SMW client - 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. --- backend/routers/import_wiki.py | 41 +++++++++++++++++++++-------- backend/smw_client.py | 47 +++++++++++++++++++++++----------- docker-compose.yml | 9 +++++++ 3 files changed, 71 insertions(+), 26 deletions(-) diff --git a/backend/routers/import_wiki.py b/backend/routers/import_wiki.py index 256d7a9..3a1a2fd 100644 --- a/backend/routers/import_wiki.py +++ b/backend/routers/import_wiki.py @@ -120,7 +120,9 @@ async def preview_import( "wiki_page_title": page_title, "wiki_page_id": page_id, "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, "warnings": warnings, "errors": errors, @@ -639,21 +641,38 @@ def _upsert_skill(mapped: dict, reimport: bool) -> Optional[int]: return skill_id -def _upsert_method(mapped: dict, reimport: bool) -> Optional[int]: - """Legt Trainingsmethode an oder aktualisiert Beschreibung.""" +def _upsert_method(mapped: dict, _reimport: bool) -> Optional[int]: + """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: cur = get_cursor(conn) cur.execute( - """INSERT INTO training_methods (name, description) - VALUES (%s, %s) - ON CONFLICT (name) DO UPDATE SET - description = EXCLUDED.description - RETURNING id""", - (mapped["name"], mapped.get("description")) + "SELECT id FROM training_methods WHERE TRIM(name) ILIKE TRIM(%s)", + (name,), ) - 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() - return method_id + return mid def _upsert_wiki_ref( diff --git a/backend/smw_client.py b/backend/smw_client.py index 7390c17..0409bdb 100644 --- a/backend/smw_client.py +++ b/backend/smw_client.py @@ -48,7 +48,11 @@ class SmwClient: "format": "json", }) 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) # Schritt 2: Einloggen @@ -56,10 +60,10 @@ class SmwClient: "action": "login", "lgname": self.user, "lgpassword": self.password, - "lgtoken": token, + "lgtoken": tok, }, cookies=cookies) r2.raise_for_status() - result = r2.json() + result = self._parse_mw_response(r2.json()) if result.get("login", {}).get("result") != "Success": reason = result.get("login", {}).get("reason", "unbekannt") @@ -69,15 +73,31 @@ class SmwClient: self._logged_in = True 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: """Authentifizierter GET-Request gegen die API.""" if not self._logged_in: await self.login() params["format"] = "json" - async with httpx.AsyncClient(timeout=30, cookies=self._cookies) as client: - r = await client.get(self.api_url, params=params) - r.raise_for_status() - return r.json() + try: + async with httpx.AsyncClient(timeout=30, cookies=self._cookies) as client: + r = await client.get(self.api_url, params=params) + r.raise_for_status() + return self._parse_mw_response(r.json()) + except SmwClientError: + raise + except Exception as e: + raise SmwClientError(str(e)) # ------------------------------------------------------------------ # # Kategorien # @@ -104,7 +124,8 @@ class SmwClient: params["cmcontinue"] = cmcontinue 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: cmcontinue = data["continue"].get("cmcontinue") @@ -143,7 +164,8 @@ class SmwClient: params["cmcontinue"] = cmcontinue 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: cmcontinue = data["continue"].get("cmcontinue") @@ -193,12 +215,9 @@ class SmwClient: "action": "browsebysubject", "subject": title, }) - if "error" in data: - raise SmwClientError(f"SMW Browse-Fehler für '{title}': {data['error']}") - # Normalisiere: {property_label: [wert1, wert2]} 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", "") if prop_label.startswith("_"): # Interne SMW-Properties überspringen continue @@ -223,8 +242,6 @@ class SmwClient: "action": "ask", "query": f"{query}|limit={limit}", }) - if "error" in data: - raise SmwClientError(f"SMW Ask-Fehler: {data['error']}") results = [] for title, props in data.get("query", {}).get("results", {}).items(): diff --git a/docker-compose.yml b/docker-compose.yml index 1f4dc53..78084c4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -42,6 +42,15 @@ services: APP_URL: "${APP_URL:-https://shinkan.jinkendo.de}" ALLOWED_ORIGINS: "${ALLOWED_ORIGINS:-https://shinkan.jinkendo.de}" 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: - shinkan-media:/app/media ports: