Compare commits
25 Commits
9947e2b429
...
fe65fafbe0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe65fafbe0 | ||
|
|
f91943cde2 | ||
|
|
e679865e50 | ||
|
|
a7f0001659 | ||
|
|
66f5eb102b | ||
|
|
4e8a5851d5 | ||
|
|
db8d5c4454 | ||
|
|
e67ef7d790 | ||
|
|
24334d1def | ||
|
|
936e209fb8 | ||
|
|
5eaa5527b6 | ||
|
|
3164c446c6 | ||
|
|
57ac6bcdd3 | ||
|
|
bd4a4bd6ac | ||
|
|
dd648bc50f | ||
|
|
4e04784f9b | ||
|
|
014cdd80e2 | ||
|
|
cc89a32eae | ||
|
|
57b0d69eae | ||
|
|
ba149b2d47 | ||
|
|
b35ab73daa | ||
|
|
1e610fbc9d | ||
|
|
46ebe53f71 | ||
|
|
569d400baf | ||
|
|
83353522db |
@@ -1,11 +1,19 @@
|
|||||||
from .slow import JSONAPI, App, Headers, HTTPResponse, JSONResponse, Request, render
|
from . import responses
|
||||||
|
from .slow import (
|
||||||
|
JSONAPI,
|
||||||
|
App,
|
||||||
|
Headers,
|
||||||
|
Request,
|
||||||
|
redirect,
|
||||||
|
render,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"JSONAPI",
|
"JSONAPI",
|
||||||
"App",
|
"App",
|
||||||
"HTTPResponse",
|
|
||||||
"JSONResponse",
|
|
||||||
"render",
|
"render",
|
||||||
"Request",
|
"Request",
|
||||||
"Headers",
|
"Headers",
|
||||||
|
"redirect",
|
||||||
|
"responses",
|
||||||
]
|
]
|
||||||
|
|||||||
38
slow/_errors.py
Normal file
38
slow/_errors.py
Normal 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"
|
||||||
|
)
|
||||||
54
slow/responses.py
Normal file
54
slow/responses.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import http.client
|
||||||
|
import json
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .slow import App
|
||||||
|
|
||||||
|
|
||||||
|
class Response:
|
||||||
|
status: int
|
||||||
|
headers: list[str]
|
||||||
|
body: bytes
|
||||||
|
|
||||||
|
def __init__(self, status: int, headers: list[str], body: bytes):
|
||||||
|
self.status = status
|
||||||
|
self.headers = headers
|
||||||
|
self.body = body
|
||||||
|
self.no_header = False
|
||||||
|
|
||||||
|
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"Content-Length: {len(self.body)}\r\n"
|
||||||
|
for h in self.headers:
|
||||||
|
header += h + "\r\n"
|
||||||
|
|
||||||
|
header += "\r\n"
|
||||||
|
|
||||||
|
return header.encode(encoding="utf-8")
|
||||||
|
|
||||||
|
def render(self, app: "App") -> bytes:
|
||||||
|
if self.no_header:
|
||||||
|
return self.header(app)
|
||||||
|
return self.header(app) + self.body
|
||||||
|
|
||||||
|
|
||||||
|
def HTTPResponse(
|
||||||
|
content: str | bytes,
|
||||||
|
status=200,
|
||||||
|
content_type="text/plain; charset=utf-8",
|
||||||
|
headers=[],
|
||||||
|
) -> Response:
|
||||||
|
if isinstance(content, str):
|
||||||
|
content_bytes = content.encode(encoding="utf-8")
|
||||||
|
else:
|
||||||
|
content_bytes = content
|
||||||
|
return Response(
|
||||||
|
status, [f"Content-Type: {content_type}", *headers], body=content_bytes
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def JSONResponse(d: dict, status=200) -> Response:
|
||||||
|
return HTTPResponse(
|
||||||
|
json.dumps(d), status=status, content_type="text/json; charset=utf-8"
|
||||||
|
)
|
||||||
207
slow/slow.py
207
slow/slow.py
@@ -1,14 +1,28 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import http.client
|
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
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
|
||||||
|
|
||||||
PR = re.compile(r"\<([a-zA-Z_][a-zA-Z0-9_]*)\>")
|
PR = re.compile(r"\<([a-zA-Z_][a-zA-Z0-9_]*)\>")
|
||||||
|
|
||||||
|
|
||||||
|
class CORS:
|
||||||
|
Origins: set[str]
|
||||||
|
Methods: set[str]
|
||||||
|
Disabled: bool
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.Disabled = False
|
||||||
|
self.Origins: set[str] = {}
|
||||||
|
self.Methods: set[str] = {"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD"}
|
||||||
|
|
||||||
|
|
||||||
class Headers:
|
class Headers:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._d: dict[str, str] = {}
|
self._d: dict[str, str] = {}
|
||||||
@@ -22,6 +36,9 @@ class Headers:
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self._d)
|
return str(self._d)
|
||||||
|
|
||||||
|
def __contains__(self, key):
|
||||||
|
return self._d.__contains__(key)
|
||||||
|
|
||||||
|
|
||||||
class Request:
|
class Request:
|
||||||
def __init__(self, method: str, path: str, headers: Headers, body: bytes):
|
def __init__(self, method: str, path: str, headers: Headers, body: bytes):
|
||||||
@@ -40,44 +57,55 @@ class Request:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def json(self) -> dict | None:
|
||||||
|
try:
|
||||||
|
return json.loads(self.body)
|
||||||
|
except JSONDecodeError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
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[bytes]]]
|
re.Pattern[str],
|
||||||
|
dict[
|
||||||
|
str,
|
||||||
|
Callable[[Request, ...], Awaitable[Response | None]],
|
||||||
|
],
|
||||||
] = {}
|
] = {}
|
||||||
self.error_routes: dict[int, Callable[[Request, ...], Awaitable[bytes]]] = {
|
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,
|
||||||
}
|
}
|
||||||
|
self.CORS = CORS()
|
||||||
|
|
||||||
def _pattern_to_regex(self, temp) -> re.Pattern[str]:
|
def _pattern_to_regex(self, temp) -> re.Pattern[str]:
|
||||||
re_temp = temp
|
re_temp = temp
|
||||||
iter = PR.finditer(temp)
|
iter = PR.finditer(temp)
|
||||||
for m in iter:
|
for m in iter:
|
||||||
name = m[1]
|
name = m[1]
|
||||||
re_temp = re.sub(
|
re_temp = re.sub(m[0], r"(?P<" + name + r">[a-zA-Z0-9\-._~:%&=]+)", re_temp)
|
||||||
m[0], r"(?P<" + name + r">[a-zA-Z0-9\-._~:%&=]+)", re_temp
|
|
||||||
)
|
|
||||||
return re.compile(re_temp)
|
return re.compile(re_temp)
|
||||||
|
|
||||||
def _serve(
|
def _serve(
|
||||||
self, path: str, method: str, func: Callable[[Request, ...], Awaitable[bytes]]
|
self,
|
||||||
|
path: str,
|
||||||
|
method: str,
|
||||||
|
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] = {}
|
||||||
@@ -88,8 +116,16 @@ 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[bytes]]):
|
def decorator(func: Callable[[Request, ...], Awaitable[Response | None]]):
|
||||||
self._serve(path, "GET", func)
|
self._serve(path, "GET", func)
|
||||||
|
|
||||||
|
async def wrapper(*args, **kwargs):
|
||||||
|
res = await func(*args, **kwargs)
|
||||||
|
if isinstance(res, Response):
|
||||||
|
res.no_header = True
|
||||||
|
return res
|
||||||
|
|
||||||
|
self._serve(path, "HEAD", wrapper)
|
||||||
return func
|
return func
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
@@ -97,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[bytes]]):
|
def decorator(func: Callable[[Request, ...], Awaitable[Response | None]]):
|
||||||
self._serve(path, "POST", func)
|
self._serve(path, "POST", func)
|
||||||
return func
|
return func
|
||||||
|
|
||||||
@@ -106,24 +142,33 @@ 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[bytes]]):
|
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[bytes]]):
|
def decorator(func: Callable[[Request, ...], Awaitable[Response | None]]):
|
||||||
self._serve(path, "DELETE", func)
|
self._serve(path, "DELETE", func)
|
||||||
return func
|
return func
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
def OPTIONS(self, path: str):
|
def PATCH(self, path: str):
|
||||||
"""Decorator to register a OPTIONS HTTP route."""
|
"""Decorator to register a PATCH HTTP route."""
|
||||||
|
|
||||||
def decorator(func: Callable[[Request, ...], Awaitable[bytes]]):
|
def decorator(func: Callable[[Request, ...], Awaitable[Response | None]]):
|
||||||
self._serve(path, "OPTIONS", func)
|
self._serve(path, "PATCH", func)
|
||||||
|
return func
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
def HEAD(self, path: str):
|
||||||
|
"""Decorator to register a HEAD HTTP route."""
|
||||||
|
|
||||||
|
def decorator(func: Callable[[Request, ...], Awaitable[Response | None]]):
|
||||||
|
self._serve(path, "HEAD", func)
|
||||||
return func
|
return func
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
@@ -137,16 +182,22 @@ class App:
|
|||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
def resolve(self, path, method) -> tuple[Callable[..., Awaitable[bytes]], 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]:
|
||||||
|
for pattern, route in self.routes.items():
|
||||||
|
if pattern.fullmatch(path):
|
||||||
|
return set(route.keys() - {"websocket"})
|
||||||
|
return set()
|
||||||
|
|
||||||
async def handle_client(
|
async def handle_client(
|
||||||
self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
|
self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
|
||||||
):
|
):
|
||||||
@@ -157,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:
|
||||||
@@ -179,21 +236,70 @@ class App:
|
|||||||
content_length = int(headers.get("Content-Length", 0))
|
content_length = int(headers.get("Content-Length", 0))
|
||||||
body = await reader.read(content_length) if content_length else b""
|
body = await reader.read(content_length) if content_length else b""
|
||||||
|
|
||||||
route, kwargs = self.resolve(path, method)
|
if (
|
||||||
|
method == "OPTIONS"
|
||||||
|
): # FIXME Should handle responding with available methods as well
|
||||||
|
if "origin" in headers and headers.get("origin") in self.CORS.Origins:
|
||||||
|
origin = headers.get("origin")
|
||||||
|
head = [
|
||||||
|
"Content-Type: text/plain",
|
||||||
|
"Content-Length: 0",
|
||||||
|
f"Access-Control-Allow-Origin: {origin}",
|
||||||
|
f"Access-Control-Allow-Methods: {','.join(self.CORS.Methods)}",
|
||||||
|
"Access-Control-Allow-Headers: Content-Type,Authorization", # CORS
|
||||||
|
"Vary: Origin",
|
||||||
|
]
|
||||||
|
|
||||||
response = await route(
|
writer.write(Response(200, head, b"").render(self))
|
||||||
request=Request(
|
|
||||||
method=method,
|
|
||||||
path=path,
|
|
||||||
headers=headers,
|
|
||||||
body=body,
|
|
||||||
),
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
writer.write(response)
|
|
||||||
|
|
||||||
await writer.drain()
|
await writer.drain()
|
||||||
|
|
||||||
|
else:
|
||||||
|
writer.write(Response(403, ["Vary: Origin"], b"").render(self))
|
||||||
|
|
||||||
|
await writer.drain()
|
||||||
|
|
||||||
|
else:
|
||||||
|
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(
|
||||||
|
request=Request(
|
||||||
|
method=method, path=path, headers=headers, body=body
|
||||||
|
),
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
response.headers.append("Vary: Origin")
|
||||||
|
if (
|
||||||
|
"origin" in headers
|
||||||
|
and self.CORS.Disabled
|
||||||
|
and headers.get("origin") in self.CORS.Origins
|
||||||
|
): # CORS
|
||||||
|
response.headers.append(
|
||||||
|
f"Access-Control-Allow-Origin: {headers.get('origin')}"
|
||||||
|
)
|
||||||
|
response.headers.append(
|
||||||
|
f"Access-Control-Allow-Methods: {','.join(self.CORS.Methods & self.methods(path))}"
|
||||||
|
)
|
||||||
|
response.headers.append(
|
||||||
|
"Access-Control-Allow-Headers: Content-Type,Authorization\r\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
writer.write(response.render(self))
|
||||||
|
|
||||||
|
await writer.drain()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
raise e
|
||||||
print(f"Internal Server Error: {e}")
|
print(f"Internal Server Error: {e}")
|
||||||
finally:
|
finally:
|
||||||
writer.close()
|
writer.close()
|
||||||
@@ -209,25 +315,12 @@ class App:
|
|||||||
await server.serve_forever()
|
await server.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
AccessControlAllowOrigin = "*"
|
|
||||||
|
|
||||||
|
|
||||||
def HTTPResponse(
|
|
||||||
content: str, status=200, content_type="text/plain; charset=utf-8", headers=[]
|
|
||||||
) -> bytes:
|
|
||||||
global AccessControlAllowOrigin
|
|
||||||
head: str = f"HTTP/1.1 {status} {http.client.responses.get(status, 'Unkown Status Code')}\r\nContent-Type: {content_type}\r\n"
|
|
||||||
head += f"Access-Control-Allow-Origin: {AccessControlAllowOrigin}\r\n" # CORS
|
|
||||||
head += "Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS\r\n" # CORS
|
|
||||||
head += "\r\n".join(headers) + ("\r\n" if len(headers) > 0 else "")
|
|
||||||
head += "\r\n"
|
|
||||||
return (head + content).encode(encoding="utf-8")
|
|
||||||
|
|
||||||
|
|
||||||
_value_pattern = re.compile(r"\{\{\s*([a-zA-Z_][a-zA-Z_0-9]*)\s*\}\}")
|
_value_pattern = re.compile(r"\{\{\s*([a-zA-Z_][a-zA-Z_0-9]*)\s*\}\}")
|
||||||
|
|
||||||
|
|
||||||
def render(file: str | Path, variables: dict[str, Any] = {}) -> bytes:
|
def render(
|
||||||
|
file: str | Path, variables: dict[str, Any] = {}
|
||||||
|
) -> Response: # TODO Move to another module
|
||||||
if isinstance(file, str):
|
if isinstance(file, str):
|
||||||
file = Path(file)
|
file = Path(file)
|
||||||
content: str = file.read_text(encoding="utf-8")
|
content: str = file.read_text(encoding="utf-8")
|
||||||
@@ -237,13 +330,11 @@ def render(file: str | Path, variables: dict[str, Any] = {}) -> bytes:
|
|||||||
return HTTPResponse(content, content_type="text/html; charset=utf-8")
|
return HTTPResponse(content, content_type="text/html; charset=utf-8")
|
||||||
|
|
||||||
|
|
||||||
def JSONResponse(d: dict, status=200) -> bytes:
|
def redirect(location: str): # TODO Move to another module
|
||||||
return HTTPResponse(
|
return Response(307, ["Location: {location}"], b"")
|
||||||
json.dumps(d), status=status, content_type="text/json; charset=utf-8"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def JSONAPI(func):
|
def JSONAPI(func): # TODO Move to another module
|
||||||
async def wrapper(*args, **kwargs):
|
async def wrapper(*args, **kwargs):
|
||||||
result = await func(*args, **kwargs)
|
result = await func(*args, **kwargs)
|
||||||
if not isinstance(result, dict):
|
if not isinstance(result, dict):
|
||||||
|
|||||||
Reference in New Issue
Block a user