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
+ ![](assets/aki.png)
14
+
15
+ # Aki
16
+ FastAPI-inspired Web Application Framework
@@ -0,0 +1,4 @@
1
+ ![](assets/aki.png)
2
+
3
+ # Aki
4
+ 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)