- Request body cap (16KB), search query cap, input range validation (year 1800-2399, date/time fields, lat/lon bounds) - Dockerfile (gunicorn, 2 workers) + compose binding 127.0.0.1:7799 for cloudflared-fronted deployment Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
113 lines
4.1 KiB
Python
113 lines
4.1 KiB
Python
"""NATEGODD — natal chart generator. Run: .venv/bin/python app.py → http://localhost:7799"""
|
|
from flask import Flask, jsonify, request, send_from_directory
|
|
|
|
import chart as chart_engine
|
|
import geo
|
|
import interp
|
|
|
|
app = Flask(__name__, static_folder="static", static_url_path="")
|
|
app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 # chart inputs are tiny; cap request bodies
|
|
|
|
|
|
@app.get("/")
|
|
def index():
|
|
return send_from_directory("static", "index.html")
|
|
|
|
|
|
@app.get("/api/search")
|
|
def api_search():
|
|
q = request.args.get("q", "")[:64]
|
|
return jsonify(geo.search_cities(q))
|
|
|
|
|
|
@app.get("/api/reference")
|
|
def api_reference():
|
|
return jsonify({"aspects": [
|
|
{"name": k, "glyph": chart_engine.ASPECT_GLYPHS[k], "geometry": v[0], "text": v[1]}
|
|
for k, v in interp.ASPECT_EXPLAINER.items()
|
|
]})
|
|
|
|
|
|
def cast(d):
|
|
"""Validate one chart-input dict and compute its chart. Raises ValueError."""
|
|
try:
|
|
year, month, day = int(d["year"]), int(d["month"]), int(d["day"])
|
|
time_unknown = bool(d.get("time_unknown"))
|
|
hour = 12 if time_unknown else int(d.get("hour", 12))
|
|
minute = 0 if time_unknown else int(d.get("minute", 0))
|
|
second = int(d.get("second", 0) or 0)
|
|
lat, lon = float(d["lat"]), float(d["lon"])
|
|
house_system = d.get("house_system", "P")
|
|
if house_system not in chart_engine.HOUSE_SYSTEMS:
|
|
house_system = "P"
|
|
except (KeyError, TypeError) as e:
|
|
raise ValueError(f"bad input: {e}")
|
|
if not (1800 <= year <= 2399):
|
|
raise ValueError("year must be 1800-2399 (ephemeris range)")
|
|
if not (1 <= month <= 12 and 1 <= day <= 31 and 0 <= hour <= 23 and 0 <= minute <= 59):
|
|
raise ValueError("date/time out of range")
|
|
if not (-90 <= lat <= 90 and -180 <= lon <= 180):
|
|
raise ValueError("coordinates out of range")
|
|
|
|
if d.get("utc_offset") not in (None, ""):
|
|
offset, tzname = float(d["utc_offset"]), "manual"
|
|
else:
|
|
try:
|
|
offset, tzname = geo.resolve_offset(lat, lon, year, month, day, hour, minute,
|
|
tz_hint=d.get("tz"))
|
|
except Exception as e:
|
|
raise ValueError(f"could not resolve timezone: {e}")
|
|
|
|
result = chart_engine.compute_chart(year, month, day, hour, minute, second,
|
|
offset, lat, lon, house_system, time_unknown)
|
|
result["input"] = {
|
|
"name": d.get("name", ""), "place": d.get("place", ""),
|
|
"lat": lat, "lon": lon, "tz": tzname, "utc_offset": offset,
|
|
"date": f"{year:04d}-{month:02d}-{day:02d}",
|
|
"time": None if time_unknown else f"{hour:02d}:{minute:02d}",
|
|
}
|
|
return result
|
|
|
|
|
|
@app.post("/api/chart")
|
|
def api_chart():
|
|
d = request.get_json(force=True)
|
|
try:
|
|
result = cast(d)
|
|
except ValueError as e:
|
|
return jsonify({"error": str(e)}), 400
|
|
except Exception as e:
|
|
return jsonify({"error": f"computation failed: {e}"}), 400
|
|
result["interpretation"] = interp.build_interpretation(result)
|
|
interp.annotate_chart(result)
|
|
return jsonify(result)
|
|
|
|
|
|
@app.post("/api/synastry")
|
|
def api_synastry():
|
|
d = request.get_json(force=True)
|
|
if not isinstance(d, dict) or "a" not in d or "b" not in d:
|
|
return jsonify({"error": "expected {a: <chart input>, b: <chart input>}"}), 400
|
|
try:
|
|
chart_a, chart_b = cast(d["a"]), cast(d["b"])
|
|
except ValueError as e:
|
|
return jsonify({"error": str(e)}), 400
|
|
except Exception as e:
|
|
return jsonify({"error": f"computation failed: {e}"}), 400
|
|
syn = chart_engine.compute_synastry(chart_a, chart_b)
|
|
na = chart_a["input"]["name"] or "A"
|
|
nb = chart_b["input"]["name"] or "B"
|
|
for x in syn["interaspects"]:
|
|
x["text"] = interp.syn_aspect_blurb(x, na, nb)
|
|
interp.annotate_chart(chart_a)
|
|
interp.annotate_chart(chart_b)
|
|
syn["interpretation"] = interp.synastry_interpretation(
|
|
syn, chart_a["input"]["name"], chart_b["input"]["name"])
|
|
syn["chart_a"] = chart_a
|
|
syn["chart_b"] = chart_b
|
|
return jsonify(syn)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app.run(host="0.0.0.0", port=7799, debug=False)
|