nercone-aki 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: nercone-aki
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: FastAPI-inspired Web Application Framework
|
|
5
|
+
Author: Nercone
|
|
6
|
+
Author-email: Nercone <nercone@nercone.dev>
|
|
7
|
+
License: MIT
|
|
8
|
+
Requires-Dist: nercone-kaede
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Project-URL: GitHub, https://github.com/nercone-aki/aki/
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
# Aki
|
|
16
|
+
FastAPI-inspired Web Application Framework
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build>=0.9.5,<0.10.0"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nercone-aki"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "FastAPI-inspired Web Application Framework"
|
|
9
|
+
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
|
+
authors = [ { name = "Nercone", email = "nercone@nercone.dev" } ]
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
dependencies = [
|
|
14
|
+
"nercone-kaede"
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
GitHub = "https://github.com/nercone-aki/aki/"
|
|
19
|
+
|
|
20
|
+
[tool.uv.build-backend]
|
|
21
|
+
module-name = "aki"
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from .app import Aki
|
|
2
|
+
from .responses import PlainTextResponse, HTMLResponse, JSONResponse, FileResponse, RedirectResponse
|
|
3
|
+
|
|
4
|
+
from kaede import Request, Response, Listener, Callback, Headers, TLS, TLSInfo, TLSServerConfig as TLSConfig, Server, ServerConfig as Config, ServerHandler as Handler, WebSocket
|
|
5
|
+
|
|
6
|
+
__all__ = ["Aki", "PlainTextResponse", "HTMLResponse", "JSONResponse", "FileResponse", "RedirectResponse", "Request", "Response", "Listener", "Callback", "Headers", "TLS", "TLSInfo", "TLSConfig", "Server", "Config", "Handler", "WebSocket"]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from kaede import Request, Response, WebSocket, Callback
|
|
2
|
+
from typing import Literal, Callable
|
|
3
|
+
from .routing import Router
|
|
4
|
+
|
|
5
|
+
class Aki(Callback):
|
|
6
|
+
def __init__(self):
|
|
7
|
+
super().__init__()
|
|
8
|
+
self.router = Router()
|
|
9
|
+
|
|
10
|
+
async def on_request(self, request: Request) -> Response:
|
|
11
|
+
return await self.router.dispatch(request, ws=None)
|
|
12
|
+
|
|
13
|
+
async def on_websocket(self, request: Request, ws: WebSocket):
|
|
14
|
+
await self.router.dispatch(request, ws=ws)
|
|
15
|
+
|
|
16
|
+
def add_route(self, path: str, *, methods: list[Literal["GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH"]] | None = None, callback: Callable) -> Callable:
|
|
17
|
+
return self.router.add_route(path, methods=methods, callback=callback)
|
|
18
|
+
|
|
19
|
+
def route(self, path: str, *, methods: list[Literal["GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH"]] | None = None) -> Callable:
|
|
20
|
+
return self.router.route(path, methods=methods)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
from kaede import Response, Headers
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
class PlainTextResponse(Response):
|
|
7
|
+
def __init__(self, content: str, *, status_code: int = 200, headers: Headers | None = None, compression: bool = True, file_range: tuple[int, int] | None = None):
|
|
8
|
+
self.body = content.encode()
|
|
9
|
+
self.status_code = status_code
|
|
10
|
+
self.headers = headers or Headers({})
|
|
11
|
+
self.content_type = "text/plain"
|
|
12
|
+
self.compression = compression
|
|
13
|
+
self.minification = False
|
|
14
|
+
self.file_range = file_range
|
|
15
|
+
|
|
16
|
+
class HTMLResponse(Response):
|
|
17
|
+
def __init__(self, content: str, *, status_code: int = 200, headers: Headers | None = None, compression: bool = True, minification: bool = False, file_range: tuple[int, int] | None = None):
|
|
18
|
+
self.body = content.encode()
|
|
19
|
+
self.status_code = status_code
|
|
20
|
+
self.headers = headers or Headers({})
|
|
21
|
+
self.content_type = "text/html"
|
|
22
|
+
self.compression = compression
|
|
23
|
+
self.minification = minification
|
|
24
|
+
self.file_range = file_range
|
|
25
|
+
|
|
26
|
+
class JSONResponse(Response):
|
|
27
|
+
def __init__(self, content: list | dict, *, status_code: int = 200, headers: Headers | None = None, compression: bool = True, file_range: tuple[int, int] | None = None):
|
|
28
|
+
self.body = json.dumps(content).encode()
|
|
29
|
+
self.status_code = status_code
|
|
30
|
+
self.headers = headers or Headers({})
|
|
31
|
+
self.content_type = "application/json"
|
|
32
|
+
self.compression = compression
|
|
33
|
+
self.minification = False
|
|
34
|
+
self.file_range = file_range
|
|
35
|
+
|
|
36
|
+
class FileResponse(Response):
|
|
37
|
+
def __init__(self, path: os.PathLike | Path, *, status_code: int = 200, headers: Headers | None = None, content_type: str | None = None, compression: bool = True, minification: bool = False, file_range: tuple[int, int] | None = None):
|
|
38
|
+
self.body = path
|
|
39
|
+
self.status_code = status_code
|
|
40
|
+
self.headers = headers or Headers({})
|
|
41
|
+
self.content_type = content_type
|
|
42
|
+
self.compression = compression
|
|
43
|
+
self.minification = minification
|
|
44
|
+
self.file_range = file_range
|
|
45
|
+
|
|
46
|
+
class RedirectResponse(Response):
|
|
47
|
+
def __init__(self, url: str, *, status_code: int = 307, headers: Headers | None = None):
|
|
48
|
+
self.body = None
|
|
49
|
+
self.status_code = status_code
|
|
50
|
+
self.headers = headers or Headers({})
|
|
51
|
+
self.content_type = None
|
|
52
|
+
self.compression = False
|
|
53
|
+
self.minification = False
|
|
54
|
+
self.file_range = None
|
|
55
|
+
|
|
56
|
+
self.headers.set("Location", url)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import inspect
|
|
3
|
+
from typing import Callable, Literal
|
|
4
|
+
from kaede import Request, Response, WebSocket
|
|
5
|
+
from .responses import PlainTextResponse
|
|
6
|
+
|
|
7
|
+
PARAM_RE = re.compile(r'\{(\w+)\}')
|
|
8
|
+
|
|
9
|
+
def compile_path(path: str) -> re.Pattern:
|
|
10
|
+
parts = PARAM_RE.split(path)
|
|
11
|
+
regex_parts = []
|
|
12
|
+
for i, part in enumerate(parts):
|
|
13
|
+
if i % 2 == 0:
|
|
14
|
+
regex_parts.append(re.escape(part))
|
|
15
|
+
else:
|
|
16
|
+
regex_parts.append(f'(?P<{part}>[^/]+)')
|
|
17
|
+
return re.compile(''.join(regex_parts))
|
|
18
|
+
|
|
19
|
+
class RouteEntry:
|
|
20
|
+
__slots__ = ('path', 'regex', 'methods', 'callback')
|
|
21
|
+
|
|
22
|
+
def __init__(self, path: str, methods: frozenset[str] | None, callback: Callable):
|
|
23
|
+
self.path = path
|
|
24
|
+
self.methods = methods
|
|
25
|
+
self.callback = callback
|
|
26
|
+
self.regex = compile_path(path)
|
|
27
|
+
|
|
28
|
+
class Router:
|
|
29
|
+
def __init__(self):
|
|
30
|
+
self.routes: list[RouteEntry] = []
|
|
31
|
+
|
|
32
|
+
def add_route(self, path: str, *, methods: list[Literal["GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH"]] | None = None, callback: Callable) -> Callable:
|
|
33
|
+
normalized = frozenset(m.upper() for m in methods) if methods else None
|
|
34
|
+
self.routes.append(RouteEntry(path, normalized, callback))
|
|
35
|
+
return callback
|
|
36
|
+
|
|
37
|
+
def route(self, path: str, *, methods: list[Literal["GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH"]] | None = None) -> Callable:
|
|
38
|
+
def decorator(func: Callable) -> Callable:
|
|
39
|
+
self.add_route(path, methods=methods, callback=func)
|
|
40
|
+
return func
|
|
41
|
+
return decorator
|
|
42
|
+
|
|
43
|
+
async def dispatch(self, request: Request, ws: WebSocket | None) -> Response | None:
|
|
44
|
+
path = request.target.split('?', 1)[0]
|
|
45
|
+
method = request.method.upper()
|
|
46
|
+
|
|
47
|
+
allowed: set[str] = set()
|
|
48
|
+
path_matched = False
|
|
49
|
+
|
|
50
|
+
for entry in self.routes:
|
|
51
|
+
match = entry.regex.fullmatch(path)
|
|
52
|
+
if match is None:
|
|
53
|
+
continue
|
|
54
|
+
path_matched = True
|
|
55
|
+
|
|
56
|
+
if entry.methods is not None and method not in entry.methods:
|
|
57
|
+
allowed.update(entry.methods)
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
result = entry.callback(request, ws, **match.groupdict())
|
|
61
|
+
if inspect.isawaitable(result):
|
|
62
|
+
result = await result
|
|
63
|
+
return result
|
|
64
|
+
|
|
65
|
+
if path_matched:
|
|
66
|
+
response = PlainTextResponse("Method Not Allowed", status_code=405)
|
|
67
|
+
response.headers.set("Allow", ", ".join(sorted(allowed)))
|
|
68
|
+
return response
|
|
69
|
+
|
|
70
|
+
return PlainTextResponse("Not Found", status_code=404)
|