Nexom 1.0.7__tar.gz → 1.0.8__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.
- {nexom-1.0.7/src/Nexom.egg-info → nexom-1.0.8}/PKG-INFO +1 -1
- {nexom-1.0.7 → nexom-1.0.8}/pyproject.toml +1 -1
- {nexom-1.0.7 → nexom-1.0.8/src/Nexom.egg-info}/PKG-INFO +1 -1
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/app/auth.py +1 -2
- nexom-1.0.8/src/nexom/app/middleware.py +135 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/app/path.py +7 -1
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/app/response.py +3 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/assets/app/templates/default.html +1 -1
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/assets/app/wsgi.py +1 -2
- nexom-1.0.7/src/nexom/app/middleware.py +0 -51
- {nexom-1.0.7 → nexom-1.0.8}/LICENSE +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/README.md +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/setup.cfg +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/Nexom.egg-info/SOURCES.txt +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/Nexom.egg-info/dependency_links.txt +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/Nexom.egg-info/entry_points.txt +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/Nexom.egg-info/requires.txt +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/Nexom.egg-info/top_level.txt +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/__init__.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/__main__.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/app/__init__.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/app/cookie.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/app/db.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/app/http_status_codes.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/app/request.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/app/template.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/app/user.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/assets/app/__init__.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/assets/app/__pycache__/__init__.cpython-313.pyc +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/assets/app/config.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/assets/app/gunicorn.conf.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/assets/app/pages/__init__.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/assets/app/pages/__pycache__/__init__.cpython-313.pyc +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/assets/app/pages/_templates.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/assets/app/pages/default.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/assets/app/pages/document.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/assets/app/router.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/assets/app/static/dog.jpeg +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/assets/app/static/github.png +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/assets/app/static/style.css +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/assets/app/templates/base.html +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/assets/app/templates/document.html +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/assets/app/templates/footer.html +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/assets/app/templates/header.html +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/assets/auth/__init__.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/assets/auth/__pycache__/__init__.cpython-313.pyc +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/assets/auth/config.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/assets/auth/gunicorn.conf.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/assets/auth/wsgi.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/assets/auth_page/login.html +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/assets/auth_page/signup.html +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/assets/error_page/error.html +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/assets/gateway/apache_app.conf +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/assets/gateway/nginx_app.conf +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/buildTools/__init__.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/buildTools/build.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/buildTools/run.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/core/__init__.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/core/error.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/core/log.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/core/object_html_render.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/templates/__init__.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/src/nexom/templates/auth.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/tests/test_buildtools.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/tests/test_http_status_codes.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/tests/test_middleware.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/tests/test_path_routing.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/tests/test_request.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/tests/test_response.py +0 -0
- {nexom-1.0.7 → nexom-1.0.8}/tests/test_static.py +0 -0
|
@@ -133,8 +133,7 @@ class AuthService:
|
|
|
133
133
|
def handler(self, environ: dict) -> JsonResponse:
|
|
134
134
|
req = Request(environ)
|
|
135
135
|
try:
|
|
136
|
-
|
|
137
|
-
return route.call_handler(req)
|
|
136
|
+
return self.routing.handle(req)
|
|
138
137
|
|
|
139
138
|
except NexomError as e:
|
|
140
139
|
# error code -> proper HTTP status
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Callable, Protocol, TypeAlias, Any
|
|
5
|
+
|
|
6
|
+
from .request import Request
|
|
7
|
+
from .response import Response
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
Handler: TypeAlias = Callable[[Request, dict[str, str | None]], Response]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Middleware(Protocol):
|
|
14
|
+
"""
|
|
15
|
+
Middleware interface.
|
|
16
|
+
|
|
17
|
+
A middleware receives the request, route args, and next handler.
|
|
18
|
+
It must return a Response.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __call__(self, request: Request, args: dict[str, str | None], next_: Handler) -> Response:
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class MiddlewareChain:
|
|
27
|
+
"""
|
|
28
|
+
Build and execute a middleware chain.
|
|
29
|
+
"""
|
|
30
|
+
middlewares: tuple[Middleware, ...]
|
|
31
|
+
|
|
32
|
+
def wrap(self, handler: Handler) -> Handler:
|
|
33
|
+
"""
|
|
34
|
+
Wrap the given handler with middlewares (outer -> inner).
|
|
35
|
+
"""
|
|
36
|
+
def wrapped(request: Request, args: dict[str, str | None]) -> Response:
|
|
37
|
+
# Build chain lazily per call (safe and simple)
|
|
38
|
+
def call_at(i: int, req: Request, a: dict[str, str | None]) -> Response:
|
|
39
|
+
if i >= len(self.middlewares):
|
|
40
|
+
return handler(req, a)
|
|
41
|
+
|
|
42
|
+
mw = self.middlewares[i]
|
|
43
|
+
|
|
44
|
+
def next_(r: Request, aa: dict[str, str | None]) -> Response:
|
|
45
|
+
return call_at(i + 1, r, aa)
|
|
46
|
+
|
|
47
|
+
return mw(req, a, next_)
|
|
48
|
+
|
|
49
|
+
return call_at(0, request, args)
|
|
50
|
+
|
|
51
|
+
return wrapped
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class CORSMiddleware:
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
allowed_origins: list[str] | None = None, # None or ["*"] = allow all
|
|
58
|
+
allowed_methods: list[str] | None = None,
|
|
59
|
+
allowed_headers: list[str] | None = None,
|
|
60
|
+
access_control_allow_credentials: bool = False,
|
|
61
|
+
max_age: int | None = 600,
|
|
62
|
+
) -> None:
|
|
63
|
+
self.allowed_origins = allowed_origins or ["*"]
|
|
64
|
+
self.allowed_methods = [m.upper() for m in (allowed_methods or ["GET", "POST", "PUT", "DELETE", "OPTIONS"])]
|
|
65
|
+
self.allowed_headers = allowed_headers or ["Content-Type", "Authorization"]
|
|
66
|
+
self.allow_credentials = access_control_allow_credentials
|
|
67
|
+
self.max_age = max_age
|
|
68
|
+
|
|
69
|
+
def _is_allowed_origin(self, origin: str) -> bool:
|
|
70
|
+
return "*" in self.allowed_origins or origin in self.allowed_origins
|
|
71
|
+
|
|
72
|
+
def _append_vary_origin(self, res: Response) -> None:
|
|
73
|
+
# append_header が単純 append なので、重複しないように軽くケア
|
|
74
|
+
for k, v in getattr(res, "headers", []):
|
|
75
|
+
if k.lower() == "vary":
|
|
76
|
+
# 既に Vary があるなら Origin が含まれてるかだけチェック
|
|
77
|
+
if "origin" in [p.strip().lower() for p in v.split(",")]:
|
|
78
|
+
return
|
|
79
|
+
res.append_header("Vary", v + ", Origin")
|
|
80
|
+
return
|
|
81
|
+
res.append_header("Vary", "Origin")
|
|
82
|
+
|
|
83
|
+
def __call__(self, request: Request, args: dict[str, str | None], next_: Handler) -> Response:
|
|
84
|
+
origin = request.headers.get("origin")
|
|
85
|
+
if not origin:
|
|
86
|
+
return next_(request, args)
|
|
87
|
+
|
|
88
|
+
if not self._is_allowed_origin(origin):
|
|
89
|
+
return next_(request, args)
|
|
90
|
+
|
|
91
|
+
# preflight 判定
|
|
92
|
+
acrm = request.headers.get("access-control-request-method")
|
|
93
|
+
is_preflight = request.method == "OPTIONS" and acrm is not None
|
|
94
|
+
|
|
95
|
+
if is_preflight:
|
|
96
|
+
# 204で十分(body無し)
|
|
97
|
+
res = Response(b"", status=204)
|
|
98
|
+
else:
|
|
99
|
+
res = next_(request, args)
|
|
100
|
+
|
|
101
|
+
# Allow-Origin(単一 or *)
|
|
102
|
+
if "*" in self.allowed_origins and not self.allow_credentials:
|
|
103
|
+
res.append_header("Access-Control-Allow-Origin", "*")
|
|
104
|
+
else:
|
|
105
|
+
# credentials=True なら必ず echo(*は禁止)
|
|
106
|
+
res.append_header("Access-Control-Allow-Origin", origin)
|
|
107
|
+
self._append_vary_origin(res)
|
|
108
|
+
|
|
109
|
+
# Allow-Credentials
|
|
110
|
+
if self.allow_credentials:
|
|
111
|
+
res.append_header("Access-Control-Allow-Credentials", "true")
|
|
112
|
+
|
|
113
|
+
# Allow-Methods
|
|
114
|
+
if self.allowed_methods == ["*"]:
|
|
115
|
+
req_method = request.headers.get("access-control-request-method")
|
|
116
|
+
res.append_header("Access-Control-Allow-Methods", req_method or "GET, POST, PUT, DELETE, OPTIONS")
|
|
117
|
+
else:
|
|
118
|
+
res.append_header("Access-Control-Allow-Methods", ", ".join(self.allowed_methods))
|
|
119
|
+
|
|
120
|
+
# Allow-Headers
|
|
121
|
+
if self.allowed_headers == ["*"]:
|
|
122
|
+
req_headers = request.headers.get("access-control-request-headers")
|
|
123
|
+
# ブラウザが要求してきたヘッダをそのまま許可
|
|
124
|
+
if req_headers:
|
|
125
|
+
res.append_header("Access-Control-Allow-Headers", req_headers)
|
|
126
|
+
else:
|
|
127
|
+
res.append_header("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
|
128
|
+
else:
|
|
129
|
+
res.append_header("Access-Control-Allow-Headers", ", ".join(self.allowed_headers))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if self.max_age is not None:
|
|
133
|
+
res.append_header("Access-Control-Max-Age", str(self.max_age))
|
|
134
|
+
|
|
135
|
+
return res
|
|
@@ -192,4 +192,10 @@ class Router(list[Path]):
|
|
|
192
192
|
|
|
193
193
|
if self.raise_if_not_exist:
|
|
194
194
|
raise PathNotFoundError(request_path)
|
|
195
|
-
return None
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
def handle(self, request: Request) -> Response:
|
|
198
|
+
path = self.get(request.path, method=request.method)
|
|
199
|
+
if path is None:
|
|
200
|
+
raise PathNotFoundError(request.path)
|
|
201
|
+
return path.call_handler(request, tuple(self.middlewares))
|
|
@@ -39,8 +39,7 @@ def app(environ: dict, start_response: Callable) -> Iterable[bytes]:
|
|
|
39
39
|
path = req.path
|
|
40
40
|
method = req.method
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
res = p.call_handler(req)
|
|
42
|
+
res = routing.handle(req)
|
|
44
43
|
|
|
45
44
|
except PathNotFoundError as e:
|
|
46
45
|
logger.warn(str(e))
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from typing import Callable, Protocol, TypeAlias, Any
|
|
5
|
-
|
|
6
|
-
from .request import Request
|
|
7
|
-
from .response import Response
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
Handler: TypeAlias = Callable[[Request, dict[str, str | None]], Response]
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class Middleware(Protocol):
|
|
14
|
-
"""
|
|
15
|
-
Middleware interface.
|
|
16
|
-
|
|
17
|
-
A middleware receives the request, route args, and next handler.
|
|
18
|
-
It must return a Response.
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
def __call__(self, request: Request, args: dict[str, str | None], next_: Handler) -> Response:
|
|
22
|
-
...
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@dataclass(frozen=True)
|
|
26
|
-
class MiddlewareChain:
|
|
27
|
-
"""
|
|
28
|
-
Build and execute a middleware chain.
|
|
29
|
-
"""
|
|
30
|
-
middlewares: tuple[Middleware, ...]
|
|
31
|
-
|
|
32
|
-
def wrap(self, handler: Handler) -> Handler:
|
|
33
|
-
"""
|
|
34
|
-
Wrap the given handler with middlewares (outer -> inner).
|
|
35
|
-
"""
|
|
36
|
-
def wrapped(request: Request, args: dict[str, str | None]) -> Response:
|
|
37
|
-
# Build chain lazily per call (safe and simple)
|
|
38
|
-
def call_at(i: int, req: Request, a: dict[str, str | None]) -> Response:
|
|
39
|
-
if i >= len(self.middlewares):
|
|
40
|
-
return handler(req, a)
|
|
41
|
-
|
|
42
|
-
mw = self.middlewares[i]
|
|
43
|
-
|
|
44
|
-
def next_(r: Request, aa: dict[str, str | None]) -> Response:
|
|
45
|
-
return call_at(i + 1, r, aa)
|
|
46
|
-
|
|
47
|
-
return mw(req, a, next_)
|
|
48
|
-
|
|
49
|
-
return call_at(0, request, args)
|
|
50
|
-
|
|
51
|
-
return wrapped
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|