Initial Commit
This commit is contained in:
176
slow/slow.py
Normal file
176
slow/slow.py
Normal file
@@ -0,0 +1,176 @@
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
PR = re.compile(r"\<([a-zA-Z_]+)\>")
|
||||
|
||||
|
||||
async def _default_404_route(request):
|
||||
return "HTTP/1.1 404 Not Found\r\nContent-Type: text/html\r\n\r\n404 Not Found contact admin"
|
||||
|
||||
|
||||
async def _default_405_route(request):
|
||||
return "HTTP/1.1 405 Method Not Allowed\r\nContent-Type: text/html\r\n\r\n405 Method Not Allowed"
|
||||
|
||||
|
||||
class App:
|
||||
def __init__(self):
|
||||
self.routes: dict[re.Pattern[str], dict[str, callable]] = {}
|
||||
self.error_routes: dict[int, callable] = {
|
||||
404: _default_404_route,
|
||||
405: _default_405_route,
|
||||
}
|
||||
|
||||
def _pattern_to_regex(self, temp) -> re.Pattern[str]:
|
||||
re_temp = temp
|
||||
iter = PR.finditer(temp)
|
||||
for m in iter:
|
||||
name = m[1]
|
||||
re_temp = re.sub(m[0], r"(?P<" + name + r">[a-zA-Z0-9_-]+)", re_temp)
|
||||
return re.compile(re_temp)
|
||||
|
||||
def _serve(self, path, method, func):
|
||||
if method not in ["GET", "POST", "PUT", "DELETE"]:
|
||||
raise RuntimeError(f'Invalid method "{method}".')
|
||||
pat = self._pattern_to_regex(path)
|
||||
if pat not in self.routes:
|
||||
self.routes[pat] = {}
|
||||
if method in self.routes[pat]:
|
||||
raise RuntimeWarning(f'Path "{path}" already exists.')
|
||||
self.routes[pat][method] = func
|
||||
|
||||
def GET(self, path):
|
||||
"""Decorator to register a GET HTTP route."""
|
||||
|
||||
def decorator(func):
|
||||
self._serve(path, "GET", func)
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
def POST(self, path):
|
||||
"""Decorator to register a POST HTTP route."""
|
||||
|
||||
def decorator(func):
|
||||
self._serve(path, "POST", func)
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
def PUT(self, path):
|
||||
"""Decorator to register a PUT HTTP route."""
|
||||
|
||||
def decorator(func):
|
||||
self._serve(path, "PUT", func)
|
||||
return func
|
||||
|
||||
def DELETE(self, path):
|
||||
"""Decorator to register a DELETE HTTP route."""
|
||||
|
||||
def decorator(func):
|
||||
self._serve(path, "DELETE", func)
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
def error(self, code):
|
||||
"""Decorator to register an error route."""
|
||||
|
||||
def decorator(func):
|
||||
self.error_routes[code] = func
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
def resolve(self, path, method) -> tuple[callable, dict]:
|
||||
for pattern, route in self.routes.items():
|
||||
if m := pattern.fullmatch(path):
|
||||
if method not in route:
|
||||
return self.error_routes[405], {}
|
||||
return route[method], m.groupdict()
|
||||
return self.error_routes[404], {}
|
||||
|
||||
async def handle_client(
|
||||
self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
|
||||
):
|
||||
"""Handle an incoming connection (HTTP or WebSocket)."""
|
||||
try:
|
||||
# Read the initial HTTP request line
|
||||
request_line = await reader.readline()
|
||||
if not request_line:
|
||||
return
|
||||
|
||||
# Parse request line
|
||||
parts = request_line.decode(encoding="utf-8").strip().split()
|
||||
if len(parts) < 3:
|
||||
return
|
||||
|
||||
method, path, protocol = parts[0], parts[1], parts[2]
|
||||
|
||||
assert protocol == "HTTP/1.1"
|
||||
|
||||
headers = {}
|
||||
|
||||
content_length = int(headers.get("Content-Length", 0))
|
||||
body = await reader.read(content_length) if content_length else b""
|
||||
|
||||
route, kwargs = self.resolve(path)
|
||||
response = await route(
|
||||
request={
|
||||
"method": method,
|
||||
"path": path,
|
||||
"headers": headers,
|
||||
"body": body,
|
||||
},
|
||||
**kwargs,
|
||||
)
|
||||
writer.write(response.encode(encoding="utf-8"))
|
||||
|
||||
await writer.drain()
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
finally:
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
|
||||
async def run(self, host="127.0.0.1", port=80):
|
||||
"""Start the async server."""
|
||||
|
||||
server = await asyncio.start_server(self.handle_client, host, port)
|
||||
|
||||
print(f"Serving on {host}:{port}")
|
||||
async with server:
|
||||
await server.serve_forever()
|
||||
|
||||
|
||||
def HTTPResponse(content: str, status=200, content_type="text/plain; charset=utf-8"):
|
||||
return f"HTTP/1.1 {status} OK\r\nContent-Type: {content_type}\r\n\r\n{content}"
|
||||
|
||||
|
||||
def render(file: str | Path):
|
||||
if type(file) is str:
|
||||
file = Path(file)
|
||||
content: str = file.read_text()
|
||||
return HTTPResponse(content, content_type="text/html; charset=utf-8")
|
||||
|
||||
|
||||
def JSONResponse(d: dict, status=200):
|
||||
return HTTPResponse(
|
||||
json.dumps(d), status=status, content_type="text/json; charset=utf-8"
|
||||
)
|
||||
|
||||
|
||||
def JSONAPI(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
result = func(*args, **kwargs)
|
||||
if not isinstance(result, dict):
|
||||
if (
|
||||
isinstance(result, tuple)
|
||||
and len(result) == 2
|
||||
and isinstance(result[1], dict)
|
||||
and isinstance(result[0], int)
|
||||
):
|
||||
return JSONResponse(result[1], result[0])
|
||||
raise RuntimeError("Return value of JSONAPI route is not a dictionary")
|
||||
return JSONResponse(result)
|
||||
Reference in New Issue
Block a user