From 14ad2cd34ac6a53587103f44949bec0f052bb957 Mon Sep 17 00:00:00 2001 From: malice Date: Fri, 22 May 2026 14:08:18 -0600 Subject: [PATCH] recon: add /api/wiki-rewrite endpoint (extraction #5 prep, additive) (#9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per-tag HTTP wrapper over wiki_rewrite.rewrite_wiki_link so the (future) navi-places service can rewrite OSM wiki tags to local Kiwix URLs over HTTP instead of importing recon's wiki_rewrite module (which talks to Kiwix on localhost:8430 and the wiki_cache table in /opt/recon/data/place_cache.db). Companion to PR #8 (/api/wiki-enrich) — Matt picked option B (HTTP-couple the Kiwix offline-wiki rewriting too, since it matters in prod). GET /api/wiki-rewrite?tag=&value= -> 200 {url, status} where status is "local" | "public" | "original" -> 400 on missing value or unknown tag -> no 404 (unclassifiable value echoes back with status "original", mirroring rewrite_wiki_link) Public (no auth), like /api/place/* and /api/wiki-enrich. Changes (additive only): - lib/wiki_rewrite_api.py: new wiki_rewrite_bp blueprint. Thin route directly over the existing rewrite_wiki_link(tag, value) — no extraction needed (it's already a clean standalone function, unlike wiki-enrich's lookup). - lib/api.py: register the blueprint (one block). - lib/wiki_rewrite_api_test.py: 5 tests (local Kiwix hit, public fallback, unclassifiable -> original, missing value -> 400, unknown tag -> 400), stubbing check_kiwix_has_article (no Kiwix/DB), plain-assert + __main__ runner. Verified green against recon's venv (flask 3.1.2). Does NOT touch place_detail's in-process _enrich_wiki_links — that gets removed in a later PR once navi-places is live (same as PR #8). wiki_cache stays in recon's own place_cache.db post-cutover (harmless positive-cache duplication). Co-authored-by: Claude Opus 4.7 (1M context) --- lib/api.py | 4 ++ lib/wiki_rewrite_api.py | 34 +++++++++++++++++ lib/wiki_rewrite_api_test.py | 73 ++++++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+) create mode 100644 lib/wiki_rewrite_api.py create mode 100644 lib/wiki_rewrite_api_test.py diff --git a/lib/api.py b/lib/api.py index c9f991e..a245706 100644 --- a/lib/api.py +++ b/lib/api.py @@ -77,6 +77,10 @@ app.register_blueprint(geocode_bp) from .wiki_enrich_api import wiki_enrich_bp app.register_blueprint(wiki_enrich_bp) +# ── Wiki-rewrite Blueprint (extraction #5 prep — HTTP wrapper over rewrite_wiki_link) ── +from .wiki_rewrite_api import wiki_rewrite_bp +app.register_blueprint(wiki_rewrite_bp) + # ── Navigation Constants ── diff --git a/lib/wiki_rewrite_api.py b/lib/wiki_rewrite_api.py new file mode 100644 index 0000000..ae1d52e --- /dev/null +++ b/lib/wiki_rewrite_api.py @@ -0,0 +1,34 @@ +"""Wiki-rewrite API — read-only HTTP wrapper over wiki_rewrite.rewrite_wiki_link. + +Extraction #5 prep: lets the (future) navi-places service rewrite OSM wiki tags +to local Kiwix URLs over HTTP instead of importing recon's wiki_rewrite module +(which talks to Kiwix and the wiki_cache table in /opt/recon/data/place_cache.db). +Additive only — does not change place_detail's in-process `_enrich_wiki_links`. + + GET /api/wiki-rewrite?tag=&value= + +Public (no auth), matching /api/place/* and /api/wiki-enrich. 400 on missing +value or unknown tag. No 404 — an unclassifiable value returns the original +value with status "original" (mirrors rewrite_wiki_link). +""" +from flask import Blueprint, request, jsonify + +from .wiki_rewrite import rewrite_wiki_link + +wiki_rewrite_bp = Blueprint('wiki_rewrite', __name__) + +_KNOWN_TAGS = {'wikipedia', 'wikidata', 'wikivoyage', 'appropedia'} + + +@wiki_rewrite_bp.route('/api/wiki-rewrite') +def api_wiki_rewrite(): + tag = (request.args.get('tag') or '').strip().lower() + value = (request.args.get('value') or '').strip() + + if not value: + return jsonify({'error': 'value is required'}), 400 + if tag not in _KNOWN_TAGS: + return jsonify({'error': f"tag must be one of {sorted(_KNOWN_TAGS)}"}), 400 + + url, status = rewrite_wiki_link(tag, value) + return jsonify({'url': url, 'status': status}) diff --git a/lib/wiki_rewrite_api_test.py b/lib/wiki_rewrite_api_test.py new file mode 100644 index 0000000..2bc50f4 --- /dev/null +++ b/lib/wiki_rewrite_api_test.py @@ -0,0 +1,73 @@ +"""Tests for the /api/wiki-rewrite endpoint (extraction #5 prep). + +Plain-assert style (recon's venv has no pytest). Builds a minimal Flask app +with only wiki_rewrite_bp registered. Mocks `wiki_rewrite.check_kiwix_has_article` +to control the local-Kiwix-hit vs. fallback paths without touching Kiwix or the +wiki_cache DB. classify_wiki_link (pure regex) runs for real. Run with pytest, +or directly: python -m lib.wiki_rewrite_api_test +""" +from flask import Flask + +from lib import wiki_rewrite +from lib.wiki_rewrite_api import wiki_rewrite_bp + + +def _client(kiwix_hit): + """kiwix_hit: (found_bool, url) returned by a stubbed check_kiwix_has_article.""" + wiki_rewrite.check_kiwix_has_article = lambda source_type, article_id: kiwix_hit + app = Flask(__name__) + app.register_blueprint(wiki_rewrite_bp) + return app.test_client() + + +def test_local_kiwix_hit(): + url = "https://wiki.echo6.co/content/wikipedia/Filer,_Idaho" + c = _client((True, url)) + resp = c.get("/api/wiki-rewrite?tag=wikipedia&value=Filer, Idaho") + assert resp.status_code == 200, resp.status_code + d = resp.get_json() + assert d["status"] == "local" + assert d["url"] == url + + +def test_public_fallback_when_not_in_kiwix(): + c = _client((False, None)) # not in Kiwix -> canonical public URL + resp = c.get("/api/wiki-rewrite?tag=wikipedia&value=Filer") + assert resp.status_code == 200, resp.status_code + d = resp.get_json() + assert d["status"] == "public" + assert d["url"] == "https://en.wikipedia.org/wiki/Filer" + + +def test_unclassifiable_returns_original(): + # 'wikidata' requires a Q-id; a non-matching value -> classify None -> original. + c = _client((False, None)) + resp = c.get("/api/wiki-rewrite?tag=wikidata&value=not-a-qid") + assert resp.status_code == 200, resp.status_code + d = resp.get_json() + assert d["status"] == "original" + assert d["url"] == "not-a-qid" + + +def test_missing_value_400(): + c = _client((False, None)) + assert c.get("/api/wiki-rewrite?tag=wikipedia").status_code == 400 + + +def test_unknown_tag_400(): + c = _client((False, None)) + assert c.get("/api/wiki-rewrite?tag=facebook&value=x").status_code == 400 + + +if __name__ == "__main__": + failures = 0 + for _name, _fn in sorted(globals().items()): + if _name.startswith("test_") and callable(_fn): + try: + _fn() + print(f"PASS {_name}") + except Exception as exc: # noqa: BLE001 + failures += 1 + print(f"FAIL {_name}: {exc!r}") + print("OK" if failures == 0 else f"{failures} FAILED") + raise SystemExit(1 if failures else 0)