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