commit 22fb0ae0f71765c84fab301082b5424b7765ed66 Author: root Date: Fri Jan 2 01:18:27 2026 +0000 HST API v5.2 - Add als, h_maestro, graph/edges endpoints Changes: - GET/POST/DELETE /api/graph/edges for Graph Explorer - als field (ref + nombre_es) in all tag responses - h_maestro alias for mrf compatibility 🤖 Generated with Claude Code diff --git a/app.py b/app.py new file mode 100644 index 0000000..fb39206 --- /dev/null +++ b/app.py @@ -0,0 +1,277 @@ +from flask import Flask, abort, redirect, jsonify, request +from flask_cors import CORS +from datetime import datetime, timezone +import psycopg2 + +app = Flask(__name__) +CORS(app) + +DB_CONFIG = { + "host": "postgres_hst", + "port": 5432, + "database": "hst_images", + "user": "directus", + "password": "directus_hst_2024" +} + +BASE_URL = "https://tzrtech.org" +MRF_BIBLIOTECA = "b7149f9e2106c566032aeb29a26e4c6cdd5f5c16b4421025c58166ee345740d1" + +def get_db(): + return psycopg2.connect(**DB_CONFIG) + +def get_discovered_tables(): + return ['flg', 'hst', 'spe', 'vsn', 'vue'] + +def build_tag(row, table=None): + ref, nombre_es, nombre_en, rootref, grupo, mrf, img, als = row[:8] + img = (img or "").strip() + return { + "ref": ref or "", + "als": als or ref or "", + "nombre_es": nombre_es or "", + "nombre_en": nombre_en or "", + "rootref": rootref or "", + "grupo": grupo or table or "hst", + "mrf": mrf or "", + "h_maestro": mrf or "", + "img": img, + "imagen_url": f"{BASE_URL}/{img}.png" if img else None, + "thumb_url": f"{BASE_URL}/thumb/{img}.png" if img else None + } + +def get_parent(mrf): + conn = get_db() + cur = conn.cursor() + cur.execute("SELECT h_padre FROM tree WHERE h_hijo = %s", (mrf,)) + row = cur.fetchone() + cur.close() + conn.close() + return row[0] if row else None + +def get_children(mrf): + conn = get_db() + cur = conn.cursor() + cur.execute(""" + SELECT h.ref, h.nombre_es, h.nombre_en, h.rootref, h.grupo, h.mrf, h.img, h.als + FROM tree t JOIN hst h ON t.h_hijo = h.mrf WHERE t.h_padre = %s + """, (mrf,)) + children = [build_tag(row) for row in cur.fetchall()] + cur.close() + conn.close() + return children + +def get_related(mrf): + conn = get_db() + cur = conn.cursor() + cur.execute(""" + SELECT h.ref, h.nombre_es, h.nombre_en, h.rootref, h.grupo, h.mrf, h.img, h.als, g.weight + FROM graph g JOIN hst h ON (g.h_b = h.mrf AND g.h_a = %s) OR (g.h_a = h.mrf AND g.h_b = %s) + ORDER BY g.weight DESC + """, (mrf, mrf)) + related = [] + for row in cur.fetchall(): + tag = build_tag(row) + tag["weight"] = row[8] + related.append(tag) + cur.close() + conn.close() + return related + +def get_table_data(table): + conn = get_db() + cur = conn.cursor() + cur.execute(f"SELECT ref, nombre_es, nombre_en, rootref, grupo, mrf, img, als FROM {table} ORDER BY ref") + records = [build_tag(row, table) for row in cur.fetchall()] + cur.close() + conn.close() + return {"count": len(records), "records": records} + +def get_tag_by_mrf(mrf): + conn = get_db() + cur = conn.cursor() + for table in get_discovered_tables(): + try: + cur.execute(f"SELECT ref, nombre_es, nombre_en, rootref, grupo, mrf, img, als FROM {table} WHERE mrf = %s", (mrf,)) + row = cur.fetchone() + if row: + tag = build_tag(row, table) + tag["table"] = table + tag["padre_mrf"] = get_parent(mrf) + cur.close() + conn.close() + return tag + except: + conn.rollback() + cur.close() + conn.close() + return None + +@app.route("/api/index.json") +def api_index(): + tables = get_discovered_tables() + total_tags = 0 + table_data = {} + for t in tables: + try: + data = get_table_data(t) + table_data[t] = data + total_tags += data["count"] + except Exception as e: + table_data[t] = {"count": 0, "records": [], "error": str(e)} + return jsonify({ + "_meta": { + "mrf_biblioteca": MRF_BIBLIOTECA, + "nombre": "HST", + "publica": True, + "version": "5.2", + "updated": datetime.now(timezone.utc).isoformat(), + "base_url": BASE_URL, + "tables": tables, + "total_tags": total_tags + }, + **table_data + }) + +@app.route("/api/tags/") +def api_tag(mrf): + tag = get_tag_by_mrf(mrf) + if not tag: + return jsonify({"error": "not found", "mrf": mrf}), 404 + return jsonify(tag) + +@app.route("/api/tags//children") +def api_tag_children(mrf): + return jsonify({"mrf_padre": mrf, "count": len(get_children(mrf)), "children": get_children(mrf)}) + +@app.route("/api/tags//related") +def api_tag_related(mrf): + related = get_related(mrf) + return jsonify({"mrf": mrf, "count": len(related), "related": related}) + +@app.route("/api/tags") +def api_tags_search(): + grupo_param = request.args.get("grupo") + grupos_param = request.args.get("grupos") + grupos_filtro = [g.strip() for g in grupos_param.split(",")] if grupos_param else [grupo_param] if grupo_param else [] + q = request.args.get("q", "").lower() + + results = [] + conn = get_db() + cur = conn.cursor() + for table in get_discovered_tables(): + try: + cur.execute(f"SELECT ref, nombre_es, nombre_en, rootref, grupo, mrf, img, als FROM {table}") + for row in cur.fetchall(): + tag = build_tag(row, table) + if grupos_filtro and tag["grupo"] not in grupos_filtro: + continue + if q and q not in (tag["nombre_es"] or "").lower() and q not in (tag["nombre_en"] or "").lower() and q not in (tag["ref"] or "").lower(): + continue + results.append(tag) + except: + conn.rollback() + cur.close() + conn.close() + return jsonify({"biblioteca": {"mrf": MRF_BIBLIOTECA, "nombre": "HST", "publica": True}, "count": len(results), "results": results}) + +@app.route("/api/grupos") +def api_grupos(): + grupos = {} + conn = get_db() + cur = conn.cursor() + for table in get_discovered_tables(): + try: + cur.execute(f"SELECT grupo, COUNT(*) FROM {table} GROUP BY grupo") + for row in cur.fetchall(): + grupos[row[0] or table] = grupos.get(row[0] or table, 0) + row[1] + except: + conn.rollback() + cur.close() + conn.close() + return jsonify({"grupos": [{"nombre": k, "count": v} for k, v in sorted(grupos.items())]}) + +@app.route("/api/tree") +def api_tree(): + conn = get_db() + cur = conn.cursor() + cur.execute("SELECT h_padre, h_hijo FROM tree") + relations = [{"mrf_padre": r[0], "mrf_hijo": r[1]} for r in cur.fetchall()] + cur.close() + conn.close() + return jsonify({"count": len(relations), "relations": relations}) + +@app.route("/api/graph") +def api_graph(): + conn = get_db() + cur = conn.cursor() + cur.execute("SELECT h_a, h_b, weight FROM graph ORDER BY weight DESC") + relations = [{"mrf_a": r[0], "mrf_b": r[1], "weight": r[2]} for r in cur.fetchall()] + cur.close() + conn.close() + return jsonify({"count": len(relations), "relations": relations}) + +@app.route("/api/library") +def api_library_list(): + conn = get_db() + cur = conn.cursor() + cur.execute("SELECT l.h_biblioteca, COUNT(*), h.nombre_es FROM library l LEFT JOIN hst h ON l.h_biblioteca = h.mrf GROUP BY l.h_biblioteca, h.nombre_es") + libraries = [{"mrf": r[0], "count": r[1], "nombre": r[2] or ""} for r in cur.fetchall()] + cur.close() + conn.close() + return jsonify({"count": len(libraries), "libraries": libraries}) + +@app.route("/health") +def health(): + return jsonify({"status": "ok", "version": "5.2"}) + +@app.route("/api/biblioteca") +def api_biblioteca(): + return jsonify({ + "biblioteca": {"mrf": MRF_BIBLIOTECA, "nombre": "HST", "publica": True, "version": "5.2"}, + "endpoints": {"tags": "/api/tags", "grupos": "/api/grupos", "tree": "/api/tree", "graph": "/api/graph", "library": "/api/library"}, + "base_url": BASE_URL + }) + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000) + +@app.route("/api/graph/edges") +def api_graph_edges(): + conn = get_db() + cur = conn.cursor() + cur.execute("SELECT id, h_a, h_b, weight FROM graph ORDER BY weight DESC") + edges = [{"id": r[0], "source_h": r[1], "target_h": r[2], "weight": r[3], "type": "db"} for r in cur.fetchall()] + cur.close() + conn.close() + return jsonify({"count": len(edges), "edges": edges}) + +@app.route("/api/graph/edges", methods=["POST"]) +def api_graph_edges_create(): + data = request.get_json() + source_h = data.get("source_h") + target_h = data.get("target_h") + weight = data.get("weight", 0.5) + edge_type = data.get("type", "manual") + + if not source_h or not target_h: + return jsonify({"error": "source_h and target_h required"}), 400 + + conn = get_db() + cur = conn.cursor() + cur.execute("INSERT INTO graph (h_a, h_b, weight) VALUES (%s, %s, %s) RETURNING id", (source_h, target_h, weight)) + new_id = cur.fetchone()[0] + conn.commit() + cur.close() + conn.close() + return jsonify({"id": new_id, "source_h": source_h, "target_h": target_h, "weight": weight, "type": edge_type}) + +@app.route("/api/graph/edges/", methods=["DELETE"]) +def api_graph_edges_delete(edge_id): + conn = get_db() + cur = conn.cursor() + cur.execute("DELETE FROM graph WHERE id = %s", (edge_id,)) + conn.commit() + cur.close() + conn.close() + return jsonify({"deleted": edge_id})