mirror of
https://github.com/Sea-Haven-Industries/meal-order-manager.git
synced 2026-09-30 05:23:13 +00:00
* feat(api): serve meals on ECS Fargate instead of Lambda Keep the Flask app always-on with in-process jobs so CloudFront no longer fronts a cold-start API Gateway. * fix(jobs): run delayed close and reminder deliveries Wall-clock skip windows dropped the only weekly SQS attempt when Scheduler already fired in Eastern time. Dev schedules stay disabled. * fix(api): return JSON objects and stop logging job payloads Flask now jsonify-s handler dicts so API responses are not HTML, and the worker logs only event and status. * fix(ci): restore the reusable workflow so the required check is named ci / ci Inlining the job reported `ci` instead of the org ruleset's `ci / ci`. * fix(secrets): drop unused os import so ruff check passes * style: apply ruff format so ci-python-app lint passes * fix(infra): give meals its own VPC because prod has none * chore(security): re-key ALB SG checkov suppression after vpc.tf
94 lines
2.6 KiB
Python
94 lines
2.6 KiB
Python
"""Call the HMAC-protected weekly-menu publication API."""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
import urllib.error
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
OUTPUT_DIR = PROJECT_ROOT / "output"
|
|
|
|
|
|
def api_request(
|
|
api_url: str,
|
|
path: str,
|
|
method: str = "GET",
|
|
body: dict | None = None,
|
|
publish_key: str = "",
|
|
) -> dict:
|
|
if not api_url.startswith(("http://", "https://")):
|
|
raise ValueError(f"api_url must use http(s) scheme: {api_url!r}")
|
|
data = json.dumps(body).encode() if body is not None else None
|
|
headers = {}
|
|
if data is not None:
|
|
headers["Content-Type"] = "application/json"
|
|
if publish_key:
|
|
headers["X-Meals-Publish-Key"] = publish_key
|
|
http_request = urllib.request.Request(
|
|
f"{api_url.rstrip('/')}{path}",
|
|
data=data,
|
|
headers=headers,
|
|
method=method,
|
|
)
|
|
try:
|
|
with urllib.request.urlopen(http_request, timeout=30) as response:
|
|
return json.loads(response.read())
|
|
except urllib.error.HTTPError as exc:
|
|
detail = exc.read().decode(errors="replace")
|
|
raise RuntimeError(
|
|
f"Publication API returned HTTP {exc.code}: {detail}"
|
|
) from exc
|
|
|
|
|
|
def get_settings(api_url: str, publish_key: str) -> dict:
|
|
return api_request(
|
|
api_url, "/api/publish/settings", method="GET", publish_key=publish_key
|
|
)
|
|
|
|
|
|
def publish_menu(api_url: str, publish_key: str) -> dict:
|
|
files = sorted(OUTPUT_DIR.glob("menu-*.json"), reverse=True)
|
|
if not files:
|
|
raise RuntimeError("No menu JSON found. Run scrape_menu.py first.")
|
|
|
|
with files[0].open() as menu_file:
|
|
menu = json.load(menu_file)
|
|
return api_request(
|
|
api_url,
|
|
"/api/publish/menu",
|
|
method="POST",
|
|
body=menu,
|
|
publish_key=publish_key,
|
|
)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("action", choices=("settings", "publish"))
|
|
parser.add_argument("--api-url", required=True)
|
|
parser.add_argument(
|
|
"--publish-key",
|
|
default=os.environ.get("MEALS_PUBLISH_KEY", ""),
|
|
help="Shared secret for /api/publish/* (or MEALS_PUBLISH_KEY)",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
try:
|
|
result = (
|
|
get_settings(args.api_url, args.publish_key)
|
|
if args.action == "settings"
|
|
else publish_menu(args.api_url, args.publish_key)
|
|
)
|
|
except (OSError, RuntimeError, ValueError, json.JSONDecodeError) as exc:
|
|
print(f"Error: {exc}", file=sys.stderr)
|
|
return 1
|
|
|
|
print(json.dumps(result))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|