63 lines
2.5 KiB
Python
63 lines
2.5 KiB
Python
"""Local static asset and read-only PostgreSQL API server."""
|
|
from datetime import date, datetime
|
|
from decimal import Decimal
|
|
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
|
import json
|
|
import logging
|
|
import os
|
|
from pathlib import Path
|
|
from urllib.parse import parse_qs, unquote, urlparse
|
|
import psycopg
|
|
from database import connect
|
|
from sample_api import ApiError, dispatch
|
|
|
|
STATIC_ROOT=Path(__file__).resolve().parent
|
|
|
|
|
|
def encode(value):
|
|
if isinstance(value,(date,datetime)): return value.isoformat()
|
|
if isinstance(value,Decimal): return float(value)
|
|
raise TypeError(f'Unsupported JSON type: {type(value).__name__}')
|
|
|
|
|
|
class Handler(SimpleHTTPRequestHandler):
|
|
def __init__(self,*args,**kwargs):
|
|
super().__init__(*args,directory=str(STATIC_ROOT),**kwargs)
|
|
|
|
def end_headers(self):
|
|
self.send_header('Cache-Control','no-store')
|
|
super().end_headers()
|
|
|
|
def send_json(self,data,status=200):
|
|
body=json.dumps(data,default=encode,allow_nan=False).encode('utf-8')
|
|
self.send_response(status)
|
|
self.send_header('Content-Type','application/json; charset=utf-8')
|
|
self.send_header('Content-Length',str(len(body)))
|
|
self.end_headers()
|
|
self.wfile.write(body)
|
|
|
|
def do_GET(self):
|
|
parsed=urlparse(self.path)
|
|
if parsed.path.startswith('/api/'):
|
|
try: return self.send_json(dispatch(parsed.path,parse_qs(parsed.query)))
|
|
except ApiError as error: return self.send_json({'error':error.message},error.status)
|
|
except (psycopg.Error,RuntimeError):
|
|
logging.exception('Database request failed')
|
|
return self.send_json({'error':'Sample database unavailable. Start PostgreSQL and run setup-database.ps1.'},503)
|
|
if parsed.path=='/':
|
|
self.send_response(302); self.send_header('Location','/mockups/overview-mockup.html'); self.end_headers(); return
|
|
name=unquote(parsed.path.removeprefix('/mockups/'))
|
|
if not parsed.path.startswith('/mockups/') or '/' in name or '\\' in name or Path(name).suffix not in ('.html','.css','.js'):
|
|
return self.send_error(404)
|
|
self.path='/'+name
|
|
return super().do_GET()
|
|
|
|
|
|
def main():
|
|
with connect() as conn: conn.execute('SELECT 1 FROM games LIMIT 1')
|
|
port=int(os.environ.get('MOCKUP_PORT','8011'))
|
|
server=ThreadingHTTPServer(('127.0.0.1',port),Handler)
|
|
print(f'PostgreSQL mockups: http://127.0.0.1:{port}/mockups/overview-mockup.html',flush=True)
|
|
try: server.serve_forever()
|
|
except KeyboardInterrupt: server.server_close()
|