"""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: , b: }"}), 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)