Compare commits

..

3 Commits

Author SHA1 Message Date
0880
fe65fafbe0 Errors and other stuff 2026-01-21 01:55:33 +03:30
0880
f91943cde2 Fix valid method condition 2026-01-20 22:19:57 +03:30
0880
e679865e50 Fix typing error 2026-01-20 22:19:09 +03:30
3 changed files with 82 additions and 22 deletions

38
slow/_errors.py Normal file
View File

@@ -0,0 +1,38 @@
import inspect
def error_not_async(func):
if not inspect.iscoroutinefunction(func):
lines, start = inspect.getsourcelines(func)
fdef_index = 0
while fdef_index < len(lines) and not lines[fdef_index].strip().startswith(
"def"
):
fdef_index += 1
if fdef_index < len(lines):
line_num = start + fdef_index
fdef_content = lines[fdef_index].strip()
ERROR_HEADER = "\033[1m\033[91m[FATAL_EXECUTION_ERROR]: Non-Asynchronous Route Detected\033[0m"
ERROR_BODY = (
f"\n"
f'--> File "{inspect.getsourcefile(func)}", line {line_num}, in {func.__name__}\n\n'
f"\033[1m[CONSTRAINT_VIOLATION]\033[0m\n"
f" Synchronous function used where an async coroutine was required.\n"
f" \033[93mCode Traceback:\033[0m\n"
f" {line_num}: \033[93m{fdef_content}\033[0m\n\n"
f"\033[1m[SUGGESTED_PATCH]\033[0m\n"
f" Apply the 'async' keyword to the function signature:\n"
f" {line_num}: \033[92masync {fdef_content}\033[0m"
)
raise RuntimeError(ERROR_HEADER + ERROR_BODY)
else:
raise RuntimeError(
"\033[1m\033[91m[FATAL_EXECUTION_ERROR]: Non-Asynchronous Route Detected\033[0m"
)

View File

@@ -17,7 +17,7 @@ class Response:
self.body = body self.body = body
self.no_header = False self.no_header = False
def header(self, app: App) -> bytes: def header(self, app: "App") -> bytes:
header = f"HTTP/1.1 {self.status} {http.client.responses.get(self.status, 'Unkown Status Code')}\r\n" header = f"HTTP/1.1 {self.status} {http.client.responses.get(self.status, 'Unkown Status Code')}\r\n"
header += f"Content-Length: {len(self.body)}\r\n" header += f"Content-Length: {len(self.body)}\r\n"
for h in self.headers: for h in self.headers:

View File

@@ -6,6 +6,7 @@ from json.decoder import JSONDecodeError
from pathlib import Path from pathlib import Path
from typing import Any, Awaitable, Callable, Optional from typing import Any, Awaitable, Callable, Optional
from . import _errors
from .responses import HTTPResponse, JSONResponse, Response from .responses import HTTPResponse, JSONResponse, Response
PR = re.compile(r"\<([a-zA-Z_][a-zA-Z0-9_]*)\>") PR = re.compile(r"\<([a-zA-Z_][a-zA-Z0-9_]*)\>")
@@ -64,23 +65,25 @@ class Request:
async def _default_404_route(request: Request): async def _default_404_route(request: Request):
return "HTTP/1.1 404 Not Found\r\nContent-Type: text/html\r\n\r\n404 Not Found contact admin".encode( return HTTPResponse("404 Not Found", status=404)
encoding="utf-8"
)
async def _default_405_route(request: Request): async def _default_405_route(request: Request):
return "HTTP/1.1 405 Method Not Allowed\r\nContent-Type: text/html\r\n\r\n405 Method Not Allowed".encode( return HTTPResponse(b"", status=405)
encoding="utf-8"
)
class App: class App:
def __init__(self): def __init__(self):
self.routes: dict[ self.routes: dict[
re.Pattern[str], dict[str, Callable[[Request, ...], Awaitable[Response]]] re.Pattern[str],
dict[
str,
Callable[[Request, ...], Awaitable[Response | None]],
],
] = {} ] = {}
self.error_routes: dict[int, Callable[[Request, ...], Awaitable[Response]]] = { self.error_routes: dict[
int, Callable[[Request, ...], Awaitable[Response | None]]
] = {
404: _default_404_route, 404: _default_404_route,
405: _default_405_route, 405: _default_405_route,
} }
@@ -98,10 +101,11 @@ class App:
self, self,
path: str, path: str,
method: str, method: str,
func: Callable[[Request, ...], Awaitable[Response]], func: Callable[[Request, ...], Awaitable[Response | None]],
): ):
if method not in ["GET", "POST", "PUT", "DELETE"]: if method not in ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD"]:
raise RuntimeError(f'Invalid method "{method}".') raise RuntimeError(f'Invalid method "{method}".')
_errors.error_not_async(func)
pat = self._pattern_to_regex(path) pat = self._pattern_to_regex(path)
if pat not in self.routes: if pat not in self.routes:
self.routes[pat] = {} self.routes[pat] = {}
@@ -112,12 +116,13 @@ class App:
def GET(self, path: str): def GET(self, path: str):
"""Decorator to register a GET HTTP route.""" """Decorator to register a GET HTTP route."""
def decorator(func: Callable[[Request, ...], Awaitable[Response]]): def decorator(func: Callable[[Request, ...], Awaitable[Response | None]]):
self._serve(path, "GET", func) self._serve(path, "GET", func)
async def wrapper(*args, **kwargs): async def wrapper(*args, **kwargs):
res: Response = await func(*args, **kwargs) res = await func(*args, **kwargs)
res.no_header = True if isinstance(res, Response):
res.no_header = True
return res return res
self._serve(path, "HEAD", wrapper) self._serve(path, "HEAD", wrapper)
@@ -128,7 +133,7 @@ class App:
def POST(self, path: str): def POST(self, path: str):
"""Decorator to register a POST HTTP route.""" """Decorator to register a POST HTTP route."""
def decorator(func: Callable[[Request, ...], Awaitable[Response]]): def decorator(func: Callable[[Request, ...], Awaitable[Response | None]]):
self._serve(path, "POST", func) self._serve(path, "POST", func)
return func return func
@@ -137,14 +142,14 @@ class App:
def PUT(self, path: str): def PUT(self, path: str):
"""Decorator to register a PUT HTTP route.""" """Decorator to register a PUT HTTP route."""
def decorator(func: Callable[[Request, ...], Awaitable[Response]]): def decorator(func: Callable[[Request, ...], Awaitable[Response | None]]):
self._serve(path, "PUT", func) self._serve(path, "PUT", func)
return func return func
def DELETE(self, path: str): def DELETE(self, path: str):
"""Decorator to register a DELETE HTTP route.""" """Decorator to register a DELETE HTTP route."""
def decorator(func: Callable[[Request, ...], Awaitable[Response]]): def decorator(func: Callable[[Request, ...], Awaitable[Response | None]]):
self._serve(path, "DELETE", func) self._serve(path, "DELETE", func)
return func return func
@@ -153,7 +158,7 @@ class App:
def PATCH(self, path: str): def PATCH(self, path: str):
"""Decorator to register a PATCH HTTP route.""" """Decorator to register a PATCH HTTP route."""
def decorator(func: Callable[[Request, ...], Awaitable[Response]]): def decorator(func: Callable[[Request, ...], Awaitable[Response | None]]):
self._serve(path, "PATCH", func) self._serve(path, "PATCH", func)
return func return func
@@ -162,7 +167,7 @@ class App:
def HEAD(self, path: str): def HEAD(self, path: str):
"""Decorator to register a HEAD HTTP route.""" """Decorator to register a HEAD HTTP route."""
def decorator(func: Callable[[Request, ...], Awaitable[Response]]): def decorator(func: Callable[[Request, ...], Awaitable[Response | None]]):
self._serve(path, "HEAD", func) self._serve(path, "HEAD", func)
return func return func
@@ -180,17 +185,17 @@ class App:
def resolve(self, path, method) -> tuple[Callable[..., Awaitable[Response]], dict]: def resolve(self, path, method) -> tuple[Callable[..., Awaitable[Response]], dict]:
for pattern, route in self.routes.items(): for pattern, route in self.routes.items():
if m := pattern.fullmatch(path): if m := pattern.fullmatch(path):
if method not in route: if method not in route or method == "websocket":
return self.error_routes[405], {} return self.error_routes[405], {}
return route[method], { return route[method], {
k: urllib.parse.unquote(v) for k, v in m.groupdict().items() k: urllib.parse.unquote(v) for k, v in m.groupdict().items()
} } # ty:ignore[invalid-return-type]
return self.error_routes[404], {} return self.error_routes[404], {}
def methods(self, path) -> set[str]: def methods(self, path) -> set[str]:
for pattern, route in self.routes.items(): for pattern, route in self.routes.items():
if pattern.fullmatch(path): if pattern.fullmatch(path):
return set(route.keys()) return set(route.keys() - {"websocket"})
return set() return set()
async def handle_client( async def handle_client(
@@ -203,6 +208,12 @@ class App:
if not request_line: if not request_line:
return return
if not request_line.decode(encoding="utf-8").startswith(
("GET", "POST", "PUT", "HEAD", "DELETE", "PATCH", "OPTIONS")
):
# Probably WebSocket
pass
# Parse request line # Parse request line
parts = request_line.decode(encoding="utf-8").strip().split() parts = request_line.decode(encoding="utf-8").strip().split()
if len(parts) < 3: if len(parts) < 3:
@@ -251,6 +262,17 @@ class App:
else: else:
route, kwargs = self.resolve(path, method) route, kwargs = self.resolve(path, method)
if (
method == "GET"
and "connection" in headers
and headers.get("connection") == "Upgrade"
and "upgrade" in headers
):
# Upgrade
return Response(
status=426, headers=["Connection: close"], body=b""
).render(self)
response: Response = await route( response: Response = await route(
request=Request( request=Request(
method=method, path=path, headers=headers, body=body method=method, path=path, headers=headers, body=body