-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
148 lines (129 loc) · 5.44 KB
/
app.py
File metadata and controls
148 lines (129 loc) · 5.44 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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# app.py
import re
import logging
import functools
import flask
from flask import Flask, request, jsonify
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from pyscramble.unscrambler import PyScramble
# ------------------------------------------------------------------------------
# Logging Configuration
# ------------------------------------------------------------------------------
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(name)s: %(message)s',
filename='pyscramble/data/logs/app.log',
)
logger = logging.getLogger(__name__)
# ------------------------------------------------------------------------------
# Flask & PyScramble Setup
# ------------------------------------------------------------------------------
app = Flask(__name__)
app.config['JSONIFY_PRETTYPRINT_REGULAR'] = True
app.config['JSON_SORT_KEYS'] = False
pyscramble = PyScramble()
# ------------------------------------------------------------------------------
# Rate Limiting
# #TODO upgrade to proper storage for prod
# ------------------------------------------------------------------------------
limiter = Limiter(
get_remote_address,
app=app,
default_limits=["200 per hour"] # global default; adjust as needed
)
# ------------------------------------------------------------------------------
# Content length check
# ------------------------------------------------------------------------------
def check_content_length(max_content_length):
def wrapper(fn):
@functools.wraps(fn)
def decorated_view(*args, **kwargs):
if int(flask.request.headers.get('Content-Length') or 0) > max_content_length:
return flask.abort(400, description='Content-Length is too large.')
else:
return fn(*args, **kwargs)
return decorated_view
return wrapper
# ------------------------------------------------------------------------------
# Helper Function: Validate letters
# ------------------------------------------------------------------------------
def validate_letters(letters: str) -> tuple[bool, str]:
"""
Validates the letters parameter:
- Must only contain alphabetic characters
- Must be no longer than 50 characters
Returns a tuple (is_valid, error_message).
"""
if not re.match(r"^[A-Za-z]+$", letters):
return False, "Invalid input. Only alphabetic letters are allowed."
if len(letters) > 50:
return False, "Input too large. Maximum length is 50 letters."
return True, ""
# ------------------------------------------------------------------------------
# /unscramble Route
# ------------------------------------------------------------------------------
@app.route("/unscramble", methods=["GET", "POST"])
@limiter.limit("10/minute") # endpoint-specific rate limit
@check_content_length(75) # limit content length to 75 bytes
def unscramble_endpoint():
"""
GET /unscramble?letters=abcxyz
or
POST /unscramble
{
"letters": "abcxyz"
}
Returns JSON { "results": [...] } or { "error": ... }
"""
try:
if request.method == "POST":
# Attempt to parse JSON body
data = request.get_json(force=True, silent=True)
if not data or "letters" not in data:
return jsonify({"status": "error", "message": "Missing or invalid 'letters' parameter."}), 400
letters = data["letters"]
else:
# GET request: read letters from the query string
letters = request.args.get("letters", "").strip()
if not letters:
return jsonify({"status": "error", "message": "Missing 'letters' query parameter."}), 400
# Validate input
valid, error_msg = validate_letters(letters)
if not valid:
return jsonify({"status": "error", "message": error_msg}), 400
# Call the unscramble function
results = pyscramble.unscramble(scrambled_string=letters)
return jsonify({"status": "ok", "message": results}), 200
except Exception as err:
# Log the error; do not expose internal info in production
logger.error("Exception in unscramble_endpoint: %s", str(err), exc_info=True)
return jsonify({"status": "error", "message": "An internal error occurred. Please try again later."}), 500
# ------------------------------------------------------------------------------
# Home & Ping Route
# ------------------------------------------------------------------------------
@app.route("/ping", methods=["GET", "HEAD"])
@limiter.limit("10/minute") # endpoint-specific rate limit
@check_content_length(5) # limit content length to 5 bytes (post?)
def ping():
"""
GET /ping
Returns a simple 200 response for health checks.
"""
return jsonify({"status": "ok", "message":"Pong!"}), 200
@app.route("/", methods=["GET"])
@limiter.limit("10/minute") # endpoint-specific rate limit
@check_content_length(19) # limit content length to 5 bytes (post?)
def home():
"""
GET /
Returns a simple 200 response.
"""
return jsonify({"status": "ok"}), 200
# ------------------------------------------------------------------------------
# Production Run (Gunicorn / uWSGI)
# ------------------------------------------------------------------------------
if __name__ == "__main__":
# In prod, run via gunicorn/uwsgi, e.g.:
# gunicorn --bind 0.0.0.0:5000 app:app
app.run(debug=True, host="0.0.0.0", port=8080)