Production hardening + container for public hosting
- 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>
This commit is contained in:
parent
c5f848733a
commit
77a522a16f
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.venv/
|
||||||
|
.git/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.DS_Store
|
||||||
8
Dockerfile
Normal file
8
Dockerfile
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
WORKDIR /app
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt gunicorn==23.0.0
|
||||||
|
COPY . .
|
||||||
|
EXPOSE 7799
|
||||||
|
# 2 workers is plenty: chart computation is a few ms; timezonefinder/geonames load per worker
|
||||||
|
CMD ["gunicorn", "-w", "2", "-b", "0.0.0.0:7799", "--access-logfile", "-", "app:app"]
|
||||||
9
app.py
9
app.py
@ -6,6 +6,7 @@ import geo
|
|||||||
import interp
|
import interp
|
||||||
|
|
||||||
app = Flask(__name__, static_folder="static", static_url_path="")
|
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("/")
|
@app.get("/")
|
||||||
@ -15,7 +16,7 @@ def index():
|
|||||||
|
|
||||||
@app.get("/api/search")
|
@app.get("/api/search")
|
||||||
def api_search():
|
def api_search():
|
||||||
q = request.args.get("q", "")
|
q = request.args.get("q", "")[:64]
|
||||||
return jsonify(geo.search_cities(q))
|
return jsonify(geo.search_cities(q))
|
||||||
|
|
||||||
|
|
||||||
@ -41,6 +42,12 @@ def cast(d):
|
|||||||
house_system = "P"
|
house_system = "P"
|
||||||
except (KeyError, TypeError) as e:
|
except (KeyError, TypeError) as e:
|
||||||
raise ValueError(f"bad input: {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, ""):
|
if d.get("utc_offset") not in (None, ""):
|
||||||
offset, tzname = float(d["utc_offset"]), "manual"
|
offset, tzname = float(d["utc_offset"]), "manual"
|
||||||
|
|||||||
7
compose.yml
Normal file
7
compose.yml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
services:
|
||||||
|
nategodd:
|
||||||
|
build: .
|
||||||
|
container_name: nategodd
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:7799:7799" # cloudflared reaches it via localhost; never exposed directly
|
||||||
Loading…
Reference in New Issue
Block a user