Nexom 1.0.6__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.
Files changed (70) hide show
  1. {nexom-1.0.6/src/Nexom.egg-info → nexom-1.0.8}/PKG-INFO +1 -1
  2. {nexom-1.0.6 → nexom-1.0.8}/pyproject.toml +1 -1
  3. {nexom-1.0.6 → nexom-1.0.8/src/Nexom.egg-info}/PKG-INFO +1 -1
  4. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/app/auth.py +1 -2
  5. nexom-1.0.8/src/nexom/app/middleware.py +135 -0
  6. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/app/path.py +8 -2
  7. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/app/response.py +3 -0
  8. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/assets/app/static/style.css +1 -1
  9. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/assets/app/templates/default.html +1 -1
  10. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/assets/app/wsgi.py +1 -2
  11. nexom-1.0.6/src/nexom/app/middleware.py +0 -51
  12. {nexom-1.0.6 → nexom-1.0.8}/LICENSE +0 -0
  13. {nexom-1.0.6 → nexom-1.0.8}/README.md +0 -0
  14. {nexom-1.0.6 → nexom-1.0.8}/setup.cfg +0 -0
  15. {nexom-1.0.6 → nexom-1.0.8}/src/Nexom.egg-info/SOURCES.txt +0 -0
  16. {nexom-1.0.6 → nexom-1.0.8}/src/Nexom.egg-info/dependency_links.txt +0 -0
  17. {nexom-1.0.6 → nexom-1.0.8}/src/Nexom.egg-info/entry_points.txt +0 -0
  18. {nexom-1.0.6 → nexom-1.0.8}/src/Nexom.egg-info/requires.txt +0 -0
  19. {nexom-1.0.6 → nexom-1.0.8}/src/Nexom.egg-info/top_level.txt +0 -0
  20. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/__init__.py +0 -0
  21. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/__main__.py +0 -0
  22. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/app/__init__.py +0 -0
  23. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/app/cookie.py +0 -0
  24. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/app/db.py +0 -0
  25. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/app/http_status_codes.py +0 -0
  26. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/app/request.py +0 -0
  27. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/app/template.py +0 -0
  28. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/app/user.py +0 -0
  29. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/assets/app/__init__.py +0 -0
  30. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/assets/app/__pycache__/__init__.cpython-313.pyc +0 -0
  31. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/assets/app/config.py +0 -0
  32. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/assets/app/gunicorn.conf.py +0 -0
  33. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/assets/app/pages/__init__.py +0 -0
  34. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/assets/app/pages/__pycache__/__init__.cpython-313.pyc +0 -0
  35. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/assets/app/pages/_templates.py +0 -0
  36. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/assets/app/pages/default.py +0 -0
  37. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/assets/app/pages/document.py +0 -0
  38. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/assets/app/router.py +0 -0
  39. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/assets/app/static/dog.jpeg +0 -0
  40. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/assets/app/static/github.png +0 -0
  41. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/assets/app/templates/base.html +0 -0
  42. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/assets/app/templates/document.html +0 -0
  43. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/assets/app/templates/footer.html +0 -0
  44. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/assets/app/templates/header.html +0 -0
  45. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/assets/auth/__init__.py +0 -0
  46. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/assets/auth/__pycache__/__init__.cpython-313.pyc +0 -0
  47. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/assets/auth/config.py +0 -0
  48. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/assets/auth/gunicorn.conf.py +0 -0
  49. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/assets/auth/wsgi.py +0 -0
  50. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/assets/auth_page/login.html +0 -0
  51. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/assets/auth_page/signup.html +0 -0
  52. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/assets/error_page/error.html +0 -0
  53. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/assets/gateway/apache_app.conf +0 -0
  54. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/assets/gateway/nginx_app.conf +0 -0
  55. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/buildTools/__init__.py +0 -0
  56. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/buildTools/build.py +0 -0
  57. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/buildTools/run.py +0 -0
  58. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/core/__init__.py +0 -0
  59. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/core/error.py +0 -0
  60. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/core/log.py +0 -0
  61. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/core/object_html_render.py +0 -0
  62. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/templates/__init__.py +0 -0
  63. {nexom-1.0.6 → nexom-1.0.8}/src/nexom/templates/auth.py +0 -0
  64. {nexom-1.0.6 → nexom-1.0.8}/tests/test_buildtools.py +0 -0
  65. {nexom-1.0.6 → nexom-1.0.8}/tests/test_http_status_codes.py +0 -0
  66. {nexom-1.0.6 → nexom-1.0.8}/tests/test_middleware.py +0 -0
  67. {nexom-1.0.6 → nexom-1.0.8}/tests/test_path_routing.py +0 -0
  68. {nexom-1.0.6 → nexom-1.0.8}/tests/test_request.py +0 -0
  69. {nexom-1.0.6 → nexom-1.0.8}/tests/test_response.py +0 -0
  70. {nexom-1.0.6 → nexom-1.0.8}/tests/test_static.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Nexom
3
- Version: 1.0.6
3
+ Version: 1.0.8
4
4
  Summary: Lightweight Python Web Framework (WSGI)
5
5
  Author: TouriAida
6
6
  License: MIT License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "Nexom"
7
- version = "1.0.6"
7
+ version = "1.0.8"
8
8
  description = "Lightweight Python Web Framework (WSGI)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Nexom
3
- Version: 1.0.6
3
+ Version: 1.0.8
4
4
  Summary: Lightweight Python Web Framework (WSGI)
5
5
  Author: TouriAida
6
6
  License: MIT License
@@ -133,8 +133,7 @@ class AuthService:
133
133
  def handler(self, environ: dict) -> JsonResponse:
134
134
  req = Request(environ)
135
135
  try:
136
- route = self.routing.get(req.path)
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
@@ -48,7 +48,7 @@ class Path:
48
48
  self.path_args[idx] = m.group(1)
49
49
 
50
50
  if detection_index == 0:
51
- detection_index = len(path_segments)
51
+ detection_index = 0 if path == "" else len(path_segments)
52
52
 
53
53
  self.path: str = "/".join(path_segments[:detection_index])
54
54
  self.detection_range: int = detection_index
@@ -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))
@@ -62,6 +62,9 @@ class Response:
62
62
  """
63
63
  yield self.body
64
64
 
65
+ def append_header(self, key: str, value: str) -> None:
66
+ self.headers.append((key, value))
67
+
65
68
  class HtmlResponse(Response):
66
69
  def __init__(
67
70
  self,
@@ -564,7 +564,7 @@ pre {
564
564
  display: flex;
565
565
  justify-content: space-between;
566
566
  align-items: center;
567
- background: var(--hevy-dark);
567
+ background: var(--black);
568
568
  border-radius: var(--mini-radius);
569
569
  overflow: hidden;
570
570
  }
@@ -2,7 +2,7 @@
2
2
  <Insert main>
3
3
  <div class="thumbnail-container">
4
4
  <div class="message-card">
5
- <h1>Welcome Nexom v1.05 Deeeeeeeev</h1>
5
+ <h1>Welcome Nexom v1.0.8 Deeeeeeeev</h1>
6
6
  <p>How are you? I'm fine! Thank you!</p>
7
7
  </div>
8
8
  <div class="accounts-card">
@@ -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
- p = routing.get(path, method=method)
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