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
This commit is contained in:
277
app.py
Normal file
277
app.py
Normal file
@@ -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/<mrf>")
|
||||
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/<mrf>/children")
|
||||
def api_tag_children(mrf):
|
||||
return jsonify({"mrf_padre": mrf, "count": len(get_children(mrf)), "children": get_children(mrf)})
|
||||
|
||||
@app.route("/api/tags/<mrf>/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/<int:edge_id>", 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})
|
||||
Reference in New Issue
Block a user