-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
93 lines (72 loc) · 3.56 KB
/
app.py
File metadata and controls
93 lines (72 loc) · 3.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
from __future__ import annotations
import json
import os
from pathlib import Path
from string import Template
from typing import TypeAlias
from wsgiref.simple_server import make_server
from planner.engine import CateringPlanner
from planner.models import EventProfile
BASE_DIR = Path(__file__).parent
INDEX_TEMPLATE = Template((BASE_DIR / "templates" / "index.html").read_text())
STYLES = (BASE_DIR / "static" / "styles.css").read_text()
APP_JS = (BASE_DIR / "static" / "app.js").read_text()
PLANNER = CateringPlanner()
NumericInput: TypeAlias = int | float | str
def _json_response(start_response, payload: dict[str, object], status: str = "200 OK"):
body = json.dumps(payload).encode("utf-8")
headers = [("Content-Type", "application/json; charset=utf-8"), ("Content-Length", str(len(body)))]
start_response(status, headers)
return [body]
def _html_response(start_response, html: str, status: str = "200 OK"):
body = html.encode("utf-8")
headers = [("Content-Type", "text/html; charset=utf-8"), ("Content-Length", str(len(body)))]
start_response(status, headers)
return [body]
def _require_string(data: dict[str, object], key: str) -> str:
value = data[key]
if not isinstance(value, str):
raise TypeError(f"{key} must be a string")
return value
def _require_numeric(data: dict[str, object], key: str) -> NumericInput:
value = data[key]
if isinstance(value, bool) or not isinstance(value, (int, float, str)):
raise TypeError(f"{key} must be numeric")
return value
def _parse_profile(data: dict[str, object]) -> EventProfile:
return EventProfile(
event_name=_require_string(data, "event_name"),
event_type=_require_string(data, "event_type"),
invited_guests=int(_require_numeric(data, "invited_guests")),
rsvp_rate=float(_require_numeric(data, "rsvp_rate")),
duration_hours=float(_require_numeric(data, "duration_hours")),
venue_type=_require_string(data, "venue_type"),
service_style=_require_string(data, "service_style"),
beverage_focus=_require_string(data, "beverage_focus"),
meal_intensity=_require_string(data, "meal_intensity"),
weather_celsius=float(_require_numeric(data, "weather_celsius")),
travel_km=float(_require_numeric(data, "travel_km")),
budget_per_guest=float(_require_numeric(data, "budget_per_guest")),
)
def application(environ, start_response):
method = environ["REQUEST_METHOD"]
path = environ.get("PATH_INFO", "/")
if method == "GET" and path == "/":
html = INDEX_TEMPLATE.substitute(styles=STYLES, script=APP_JS)
return _html_response(start_response, html)
if method == "POST" and path == "/api/plan":
try:
content_length = int(environ.get("CONTENT_LENGTH", "0") or "0")
raw_body = environ["wsgi.input"].read(content_length)
payload = json.loads(raw_body or b"{}")
profile = _parse_profile(payload)
report = PLANNER.build_report(profile)
return _json_response(start_response, PLANNER.as_dict(report))
except (KeyError, TypeError, ValueError, json.JSONDecodeError) as exc:
return _json_response(start_response, {"error": f"Invalid request: {exc}"}, status="400 Bad Request")
return _json_response(start_response, {"error": "Not found"}, status="404 Not Found")
if __name__ == "__main__":
port = int(os.environ.get("PORT", "8000"))
with make_server("127.0.0.1", port, application) as server:
print(f"Serving on http://127.0.0.1:{port}")
server.serve_forever()