Nexom 0.1.3__py3-none-any.whl → 1.0.1__py3-none-any.whl

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 (67) hide show
  1. nexom/__init__.py +2 -2
  2. nexom/__main__.py +111 -17
  3. nexom/app/__init__.py +62 -0
  4. nexom/app/auth.py +322 -0
  5. nexom/{web → app}/cookie.py +4 -2
  6. nexom/app/db.py +88 -0
  7. nexom/app/path.py +195 -0
  8. nexom/app/request.py +267 -0
  9. nexom/{web → app}/response.py +13 -3
  10. nexom/{web → app}/template.py +1 -1
  11. nexom/app/user.py +31 -0
  12. nexom/assets/app/__init__.py +0 -0
  13. nexom/assets/app/__pycache__/__init__.cpython-313.pyc +0 -0
  14. nexom/assets/app/config.py +28 -0
  15. nexom/assets/app/gunicorn.conf.py +5 -0
  16. nexom/assets/app/pages/__pycache__/__init__.cpython-313.pyc +0 -0
  17. nexom/assets/app/pages/_templates.py +7 -0
  18. nexom/assets/{server → app}/pages/default.py +2 -2
  19. nexom/assets/{server → app}/pages/document.py +2 -2
  20. nexom/assets/app/router.py +12 -0
  21. nexom/assets/app/wsgi.py +64 -0
  22. nexom/assets/auth/__init__.py +0 -0
  23. nexom/assets/auth/__pycache__/__init__.cpython-313.pyc +0 -0
  24. nexom/assets/auth/config.py +27 -0
  25. nexom/assets/auth/gunicorn.conf.py +5 -0
  26. nexom/assets/auth/wsgi.py +62 -0
  27. nexom/assets/auth_page/login.html +95 -0
  28. nexom/assets/auth_page/signup.html +106 -0
  29. nexom/assets/error_page/error.html +3 -3
  30. nexom/assets/gateway/apache_app.conf +16 -0
  31. nexom/assets/gateway/nginx_app.conf +21 -0
  32. nexom/buildTools/__init__.py +1 -0
  33. nexom/buildTools/build.py +274 -54
  34. nexom/buildTools/run.py +185 -0
  35. nexom/core/__init__.py +2 -1
  36. nexom/core/error.py +81 -3
  37. nexom/core/log.py +111 -0
  38. nexom/{engine → core}/object_html_render.py +4 -1
  39. nexom/templates/__init__.py +0 -0
  40. nexom/templates/auth.py +72 -0
  41. {nexom-0.1.3.dist-info → nexom-1.0.1.dist-info}/METADATA +75 -50
  42. nexom-1.0.1.dist-info/RECORD +56 -0
  43. {nexom-0.1.3.dist-info → nexom-1.0.1.dist-info}/WHEEL +1 -1
  44. nexom/assets/server/config.py +0 -27
  45. nexom/assets/server/gunicorn.conf.py +0 -16
  46. nexom/assets/server/pages/__pycache__/__init__.cpython-313.pyc +0 -0
  47. nexom/assets/server/pages/_templates.py +0 -11
  48. nexom/assets/server/router.py +0 -18
  49. nexom/assets/server/wsgi.py +0 -30
  50. nexom/engine/__init__.py +0 -1
  51. nexom/web/__init__.py +0 -5
  52. nexom/web/path.py +0 -125
  53. nexom/web/request.py +0 -62
  54. nexom-0.1.3.dist-info/RECORD +0 -39
  55. /nexom/{web → app}/http_status_codes.py +0 -0
  56. /nexom/{web → app}/middleware.py +0 -0
  57. /nexom/assets/{server → app}/pages/__init__.py +0 -0
  58. /nexom/assets/{server → app}/static/dog.jpeg +0 -0
  59. /nexom/assets/{server → app}/static/style.css +0 -0
  60. /nexom/assets/{server → app}/templates/base.html +0 -0
  61. /nexom/assets/{server → app}/templates/default.html +0 -0
  62. /nexom/assets/{server → app}/templates/document.html +0 -0
  63. /nexom/assets/{server → app}/templates/footer.html +0 -0
  64. /nexom/assets/{server → app}/templates/header.html +0 -0
  65. {nexom-0.1.3.dist-info → nexom-1.0.1.dist-info}/entry_points.txt +0 -0
  66. {nexom-0.1.3.dist-info → nexom-1.0.1.dist-info}/licenses/LICENSE +0 -0
  67. {nexom-0.1.3.dist-info → nexom-1.0.1.dist-info}/top_level.txt +0 -0
nexom/__init__.py CHANGED
@@ -7,8 +7,8 @@ WSGI-based web applications with minimal overhead.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- from nexom.web.request import Request
11
- from nexom.web.response import Response
10
+ from nexom.app.request import Request
11
+ from nexom.app.response import Response
12
12
 
13
13
  __all__ = [
14
14
  "Request",
nexom/__main__.py CHANGED
@@ -4,13 +4,18 @@ import argparse
4
4
  import sys
5
5
  from pathlib import Path
6
6
 
7
- from nexom.buildTools.build import server as build_server
8
- from nexom.buildTools.build import ServerBuildOptions
7
+ from nexom.buildTools.build import (
8
+ create_app,
9
+ create_auth,
10
+ start_project,
11
+ AppBuildOptions,
12
+ )
13
+ from nexom.buildTools.run import run_project
9
14
 
10
15
 
11
16
  def main(argv: list[str] | None = None) -> None:
12
17
  parser = argparse.ArgumentParser(
13
- prog="nexom",
18
+ prog="naxom",
14
19
  description="Nexom Web Framework CLI",
15
20
  )
16
21
  subparsers = parser.add_subparsers(dest="command", required=True)
@@ -18,21 +23,63 @@ def main(argv: list[str] | None = None) -> None:
18
23
  # test
19
24
  subparsers.add_parser("test", help="Test Nexom installation")
20
25
 
21
- # build-server
22
- p = subparsers.add_parser(
23
- "build-server",
24
- help="Create a Nexom server project",
26
+ # run
27
+ pr = subparsers.add_parser("run", help="Run WSGI apps found in the current project directory")
28
+ pr.add_argument("apps", nargs="*", help="App directory names to run (default: all detected apps)")
29
+ pr.add_argument("--root", default=".", help="Project root directory (default: .)")
30
+ pr.add_argument("--dry-run", action="store_true", help="Print commands only (do not start processes)")
31
+
32
+ # start-project
33
+ sp = subparsers.add_parser(
34
+ "start-project",
35
+ help="Initialize a Nexom project in the current directory (creates app/auth/data[/gateway])",
25
36
  )
26
- p.add_argument("server_name", help="Server project name")
27
- p.add_argument(
28
- "--out",
29
- default=".",
30
- help="Output directory (default: current directory)",
37
+ sp.add_argument("--root", default=".", help="Project root directory (default: .)")
38
+ sp.add_argument("--main-name", default="app", help="Main app directory name (default: app)")
39
+ sp.add_argument("--auth-name", default="auth", help="Auth app directory name (default: auth)")
40
+ sp.add_argument(
41
+ "--gateway",
42
+ choices=["none", "nginx", "apache"],
43
+ default="none",
44
+ help="Create gateway/ and put a template config (default: none)",
31
45
  )
46
+ sp.add_argument("--domain", default="", help="Domain for gateway template (default: placeholder text)")
47
+
48
+ # ports etc (main)
49
+ sp.add_argument("--address", default="0.0.0.0", help="Bind address for main app (default: 0.0.0.0)")
50
+ sp.add_argument("--port", type=int, default=8080, help="Bind port for main app (default: 8080)")
51
+ sp.add_argument("--workers", type=int, default=4, help="Gunicorn workers for main app (default: 4)")
52
+ sp.add_argument("--reload", action="store_true", help="Enable auto-reload for main app (development)")
53
+
54
+ # ports etc (auth)
55
+ sp.add_argument("--auth-address", default="127.0.0.1", help="Bind address for auth app (default: 0.0.0.0)")
56
+ sp.add_argument("--auth-port", type=int, default=7070, help="Bind port for auth app (default: 7070)")
57
+ sp.add_argument("--auth-workers", type=int, default=4, help="Gunicorn workers for auth app (default: 4)")
58
+ sp.add_argument("--auth-reload", action="store_true", help="Enable auto-reload for auth app (development)")
59
+
60
+ # create-auth
61
+ pa = subparsers.add_parser("create-auth", help="Create a Nexom auth app project")
62
+ pa.add_argument("--out", default=".", help="Output directory (default: current directory)")
63
+ pa.add_argument("--address", default="0.0.0.0", help="Bind address (default: 0.0.0.0)")
64
+ pa.add_argument("--port", type=int, default=7070, help="Bind port (default: 7070)")
65
+ pa.add_argument("--workers", type=int, default=4, help="Gunicorn workers (default: 4)")
66
+ pa.add_argument("--reload", action="store_true", help="Enable auto-reload (development)")
67
+
68
+ # create-app
69
+ p = subparsers.add_parser("create-app", help="Create a Nexom app project")
70
+ p.add_argument("app_name", help="App project name")
71
+ p.add_argument("--out", default=".", help="Output directory (default: current directory)")
32
72
  p.add_argument("--address", default="0.0.0.0", help="Bind address (default: 0.0.0.0)")
33
73
  p.add_argument("--port", type=int, default=8080, help="Bind port (default: 8080)")
34
74
  p.add_argument("--workers", type=int, default=4, help="Gunicorn workers (default: 4)")
35
75
  p.add_argument("--reload", action="store_true", help="Enable auto-reload (development)")
76
+ p.add_argument(
77
+ "--gateway-config",
78
+ choices=["nginx", "apache"],
79
+ default="",
80
+ help="If gateway/ exists, write a config template for this app (nginx/apache)",
81
+ )
82
+ p.add_argument("--domain", default="", help="Domain for gateway template (default: placeholder text)")
36
83
 
37
84
  args = parser.parse_args(argv)
38
85
 
@@ -40,22 +87,69 @@ def main(argv: list[str] | None = None) -> None:
40
87
  print("Hello Nexom Web Framework!")
41
88
  return
42
89
 
43
- if args.command == "build-server":
44
- options = ServerBuildOptions(
90
+ if args.command == "run":
91
+ run_project(Path(args.root), list(args.apps), dry_run=bool(args.dry_run))
92
+ return
93
+
94
+ if args.command == "start-project":
95
+ main_opt = AppBuildOptions(
96
+ address=args.address,
97
+ port=args.port,
98
+ workers=args.workers,
99
+ reload=args.reload,
100
+ )
101
+ auth_opt = AppBuildOptions(
102
+ address=args.auth_address,
103
+ port=args.auth_port,
104
+ workers=args.auth_workers,
105
+ reload=args.auth_reload,
106
+ )
107
+ out = start_project(
108
+ project_root=Path(args.root),
109
+ main_name=args.main_name,
110
+ auth_name=args.auth_name,
111
+ main_options=main_opt,
112
+ auth_options=auth_opt,
113
+ gateway=args.gateway,
114
+ domain=args.domain,
115
+ )
116
+ print(f"Initialized Nexom project at: {out}")
117
+ return
118
+
119
+ if args.command == "create-app":
120
+ options = AppBuildOptions(
121
+ address=args.address,
122
+ port=args.port,
123
+ workers=args.workers,
124
+ reload=args.reload,
125
+ )
126
+ out_dir = create_app(
127
+ Path(args.out),
128
+ args.app_name,
129
+ options=options,
130
+ gateway_config=(args.gateway_config or None),
131
+ domain=args.domain,
132
+ )
133
+ print(f"Created Nexom app project at: {out_dir}")
134
+ return
135
+
136
+ if args.command == "create-auth":
137
+ options = AppBuildOptions(
45
138
  address=args.address,
46
139
  port=args.port,
47
140
  workers=args.workers,
48
141
  reload=args.reload,
49
142
  )
50
- out_dir = build_server(Path(args.out), args.server_name, options=options)
51
- print(f"Created Nexom server project at: {out_dir}")
143
+ out_dir = create_auth(Path(args.out), options=options)
144
+ print(f"Created Nexom auth app project at: {out_dir}")
52
145
  return
53
146
 
54
147
 
55
148
  if __name__ == "__main__":
56
149
  try:
57
150
  main()
151
+ except KeyboardInterrupt:
152
+ sys.exit(130)
58
153
  except Exception as e:
59
- # CLI では stacktrace よりまずメッセージ優先(必要なら後で --verbose とか足す)
60
154
  print(f"Error: {e}", file=sys.stderr)
61
155
  sys.exit(1)
nexom/app/__init__.py ADDED
@@ -0,0 +1,62 @@
1
+ """
2
+ Nexom application layer public API.
3
+
4
+ This module exposes the stable interfaces intended for application developers.
5
+ Internal implementation details should NOT be imported directly.
6
+ """
7
+
8
+ # ---- Request / Response ----
9
+ from .request import Request
10
+ from .response import (
11
+ Response,
12
+ HtmlResponse,
13
+ JsonResponse,
14
+ Redirect,
15
+ ErrorResponse,
16
+ )
17
+
18
+ # ---- Routing ----
19
+ from .path import Path, Static, Pathlib
20
+
21
+ # ---- Cookie ----
22
+ from .cookie import Cookie, RequestCookies
23
+
24
+ # ---- Templates ----
25
+ from .template import ObjectHTMLTemplates
26
+
27
+ # ---- Auth ----
28
+ from .auth import AuthService, AuthClient
29
+
30
+ # ---- Middleware ----
31
+ from .middleware import Middleware, MiddlewareChain
32
+
33
+
34
+ __all__ = [
35
+ # request / response
36
+ "Request",
37
+ "Response",
38
+ "HtmlResponse",
39
+ "JsonResponse",
40
+ "Redirect",
41
+ "ErrorResponse",
42
+
43
+ # routing
44
+ "Path",
45
+ "Static",
46
+ "Pathlib",
47
+
48
+ # cookie
49
+ "Cookie",
50
+ "RequestCookies",
51
+
52
+ # templates
53
+ "ObjectHTMLTemplates",
54
+
55
+ # auth
56
+ "AuthService",
57
+ "AuthVerify",
58
+
59
+ # middleware
60
+ "Middleware",
61
+ "MiddlewareChain",
62
+ ]
nexom/app/auth.py ADDED
@@ -0,0 +1,322 @@
1
+ # src/nexom/app/auth.py
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from typing import Optional, override
6
+ import secrets
7
+ import time
8
+ import hashlib
9
+ import hmac
10
+ import json
11
+ from urllib.request import Request as UrlRequest, urlopen
12
+ from urllib.error import URLError, HTTPError
13
+
14
+ from .request import Request
15
+ from .response import JsonResponse
16
+ from .db import DatabaseManager
17
+ from .path import Path, Pathlib
18
+ from ..core.log import AuthLogger
19
+
20
+ from ..core.error import (
21
+ NexomError,
22
+ AuthMissingFieldError,
23
+ AuthInvalidCredentialsError,
24
+ AuthUserDisabledError,
25
+ AuthTokenInvalidError,
26
+ )
27
+
28
+
29
+ # --------------------
30
+ # utils
31
+ # --------------------
32
+
33
+ def _now() -> int:
34
+ return int(time.time())
35
+
36
+ def _rand(nbytes: int = 24) -> str:
37
+ return secrets.token_urlsafe(nbytes)
38
+
39
+ def _make_salt(nbytes: int = 16) -> str:
40
+ return secrets.token_hex(nbytes)
41
+
42
+ def _hash_password(password: str, salt_hex: str) -> str:
43
+ salt = bytes.fromhex(salt_hex)
44
+ dk = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, 200_000)
45
+ return dk.hex()
46
+
47
+ def _token_hash(token: str) -> str:
48
+ return hashlib.sha256(token.encode("utf-8")).hexdigest()
49
+
50
+ # --------------------
51
+ # models (internal)
52
+ # --------------------
53
+
54
+ @dataclass
55
+ class Session:
56
+ sid: str
57
+ uid: str
58
+ user_id: str
59
+ token: str
60
+ expires_at: int
61
+ revoked_at: int | None
62
+ user_agent: str | None
63
+
64
+ # --------------------
65
+ # AuthService (API only)
66
+ # --------------------
67
+
68
+ class AuthService:
69
+ """
70
+ Auth API service (JSON only).
71
+ """
72
+
73
+ def __init__(self, db_path: str, log_path: str, *, ttl_sec: int = 60 * 60 * 24 * 7, prefix: str = "") -> None:
74
+ self.dbm = AuthDBM(db_path)
75
+ self.ttl_sec = ttl_sec
76
+
77
+ p = prefix.strip("/")
78
+ def _p(x: str) -> str:
79
+ return f"{p}/{x}".strip("/") if p else x
80
+
81
+ self.routing = Pathlib(
82
+ Path(_p("signup"), self.signup, "AuthSignup"),
83
+ Path(_p("login"), self.login, "AuthLogin"),
84
+ Path(_p("logout"), self.logout, "AuthLogout"),
85
+ Path(_p("verify"), self.verify, "AuthVerify"),
86
+ )
87
+
88
+ self.logger = AuthLogger(log_path)
89
+
90
+ def handler(self, environ: dict) -> JsonResponse:
91
+ req = Request(environ)
92
+ try:
93
+ route = self.routing.get(req.path)
94
+ return route.call_handler(req)
95
+ except NexomError as e:
96
+ return JsonResponse({"ok": False, "error": e.code}, status=400)
97
+ except Exception:
98
+ return JsonResponse({"ok": False, "error": "InternalError"}, status=500)
99
+
100
+ # ---- handlers ----
101
+
102
+ def signup(self, request: Request, args: dict[str, Optional[str]]) -> JsonResponse:
103
+ if request.method != "POST":
104
+ return JsonResponse({"ok": False}, status=405)
105
+
106
+ data = request.json() or {}
107
+ self.dbm.signup(
108
+ user_id=str(data.get("user_id") or "").strip(),
109
+ public_name=str(data.get("public_name") or "").strip(),
110
+ password=str(data.get("password") or ""),
111
+ )
112
+ return JsonResponse({"ok": True}, status=201)
113
+
114
+ def login(self, request: Request, args: dict[str, Optional[str]]) -> JsonResponse:
115
+ if request.method != "POST":
116
+ return JsonResponse({"ok": False}, status=405)
117
+
118
+ data = request.json() or {}
119
+ sess = self.dbm.login(
120
+ str(data.get("user_id") or "").strip(),
121
+ str(data.get("password") or ""),
122
+ user_agent=request.headers.get("user-agent"),
123
+ ttl_sec=self.ttl_sec,
124
+ )
125
+
126
+ return JsonResponse({
127
+ "ok": True,
128
+ "user_id": sess.user_id,
129
+ "token": sess.token,
130
+ "expires_at": sess.expires_at,
131
+ })
132
+
133
+ def logout(self, request: Request, args: dict[str, Optional[str]]) -> JsonResponse:
134
+ if request.method != "POST":
135
+ return JsonResponse({"ok": False}, status=405)
136
+
137
+ token = str((request.json() or {}).get("token") or "")
138
+ if token:
139
+ self.dbm.logout(token)
140
+ return JsonResponse({"ok": True})
141
+
142
+ def verify(self, request: Request, args: dict[str, Optional[str]]) -> JsonResponse:
143
+ if request.method != "POST":
144
+ return JsonResponse({"ok": False}, status=405)
145
+
146
+ token = str((request.json() or {}).get("token") or "")
147
+ sess = self.dbm.verify(token)
148
+ if not sess:
149
+ return JsonResponse({"active": False})
150
+
151
+ return JsonResponse({
152
+ "active": True,
153
+ "user_id": sess.user_id,
154
+ "expires_at": sess.expires_at,
155
+ })
156
+
157
+ # --------------------
158
+ # AuthClient (App側)
159
+ # --------------------
160
+
161
+ class AuthClient:
162
+ """AuthService を HTTP で叩くクライアント"""
163
+
164
+ def __init__(self, auth_url: str, *, timeout: float = 3.0) -> None:
165
+ base = auth_url.rstrip("/")
166
+ self.signup_url = base + "/signup"
167
+ self.login_url = base + "/login"
168
+ self.logout_url = base + "/logout"
169
+ self.verify_url = base + "/verify"
170
+ self.timeout = timeout
171
+
172
+ def _post(self, url: str, body: dict) -> dict:
173
+ payload = json.dumps(body).encode("utf-8")
174
+ req = UrlRequest(
175
+ url,
176
+ data=payload,
177
+ headers={"Content-Type": "application/json"},
178
+ method="POST",
179
+ )
180
+ try:
181
+ with urlopen(req, timeout=self.timeout) as r:
182
+ data = json.loads(r.read().decode("utf-8"))
183
+ except (HTTPError, URLError, json.JSONDecodeError) as e:
184
+ print(str(e))
185
+ raise AuthTokenInvalidError()
186
+ return data
187
+
188
+ def signup(self, *, user_id: str, public_name: str, password: str) -> bool:
189
+ return bool(self._post(self.signup_url, {
190
+ "user_id": user_id,
191
+ "public_name": public_name,
192
+ "password": password,
193
+ }).get("ok"))
194
+
195
+ def login(self, *, user_id: str, password: str) -> tuple[str, str, int]:
196
+ d = self._post(self.login_url, {"user_id": user_id, "password": password})
197
+ return d["token"], d["user_id"], d["expires_at"]
198
+
199
+ def verify_token(self, *, token: str) -> tuple[bool, Optional[str], Optional[int]]:
200
+ d = self._post(self.verify_url, {"token": token})
201
+ if not d.get("active"):
202
+ return False, None, None
203
+ return True, d["user_id"], d["expires_at"]
204
+
205
+ def logout(self, *, token: str) -> bool:
206
+ return bool(self._post(self.logout_url, {"token": token}).get("ok"))
207
+
208
+ # --------------------
209
+ # DB
210
+ # --------------------
211
+
212
+ class AuthDBM(DatabaseManager):
213
+ @override
214
+ def _init(self) -> None:
215
+ self.execute_many(
216
+ [
217
+ (
218
+ """
219
+ CREATE TABLE IF NOT EXISTS users (
220
+ uid TEXT PRIMARY KEY,
221
+ user_id TEXT UNIQUE NOT NULL,
222
+ public_name TEXT NOT NULL,
223
+ password_hash TEXT NOT NULL,
224
+ password_salt TEXT NOT NULL,
225
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
226
+ is_active INTEGER NOT NULL DEFAULT 1
227
+ );
228
+ """,
229
+ (),
230
+ ),
231
+ (
232
+ """
233
+ CREATE TABLE IF NOT EXISTS sessions (
234
+ sid TEXT PRIMARY KEY,
235
+ uid TEXT NOT NULL REFERENCES users(uid),
236
+ token_hash TEXT UNIQUE NOT NULL,
237
+ expires_at INTEGER NOT NULL,
238
+ revoked_at INTEGER,
239
+ user_agent TEXT
240
+ );
241
+ """,
242
+ (),
243
+ )
244
+ ]
245
+ )
246
+
247
+ def signup(self, user_id: str, public_name: str, password: str) -> None:
248
+ if not user_id:
249
+ raise AuthMissingFieldError("user_id")
250
+ if not public_name:
251
+ raise AuthMissingFieldError("public_name")
252
+ if not password:
253
+ raise AuthMissingFieldError("password")
254
+
255
+ salt = _make_salt()
256
+ self.execute(
257
+ "INSERT INTO users VALUES(?,?,?,?,?,?,?)",
258
+ _rand(),
259
+ user_id,
260
+ public_name,
261
+ _hash_password(password, salt),
262
+ salt,
263
+ None,
264
+ 1,
265
+ )
266
+
267
+ def login(self, user_id: str, password: str, *, user_agent: str | None, ttl_sec: int) -> Session:
268
+ rows = self.execute(
269
+ "SELECT uid, user_id, password_hash, password_salt, is_active FROM users WHERE user_id=?",
270
+ user_id,
271
+ )
272
+ if not rows:
273
+ raise AuthInvalidCredentialsError()
274
+
275
+ uid, uid_text, pw_hash, salt, active = rows[0]
276
+ if not active:
277
+ raise AuthUserDisabledError()
278
+ if not hmac.compare_digest(_hash_password(password, salt), pw_hash):
279
+ raise AuthInvalidCredentialsError()
280
+
281
+ token = _rand()
282
+ exp = _now() + ttl_sec
283
+
284
+ self.execute(
285
+ "INSERT INTO sessions VALUES(?,?,?,?,?,?)",
286
+ _rand(),
287
+ uid,
288
+ _token_hash(token),
289
+ exp,
290
+ None,
291
+ user_agent,
292
+ )
293
+
294
+ return Session("", uid, uid_text, token, exp, None, user_agent)
295
+
296
+ def logout(self, token: str) -> None:
297
+ self.execute(
298
+ "UPDATE sessions SET revoked_at=? WHERE token_hash=?",
299
+ _now(),
300
+ _token_hash(token),
301
+ )
302
+
303
+ def verify(self, token: str | None) -> Session | None:
304
+ if not token:
305
+ return None
306
+
307
+ rows = self.execute(
308
+ """
309
+ SELECT s.sid, s.uid, u.user_id, s.expires_at, s.revoked_at, s.user_agent
310
+ FROM sessions s JOIN users u ON u.uid=s.uid
311
+ WHERE s.token_hash=?
312
+ """,
313
+ _token_hash(token),
314
+ )
315
+ if not rows:
316
+ return None
317
+
318
+ sid, uid, user_id, exp, rev, ua = rows[0]
319
+ if rev or exp <= _now():
320
+ return None
321
+
322
+ return Session(sid, uid, user_id, token, exp, None, ua)
@@ -69,5 +69,7 @@ class RequestCookies(dict[str, str | None]):
69
69
  super().__init__(kwargs)
70
70
  self.default: str | None = None
71
71
 
72
- def get(self, key: str) -> str | None:
73
- return super().get(key, self.default)
72
+ def get(self, key: str, default: str | None = None) -> str | None:
73
+ if default is None:
74
+ default = self.default
75
+ return super().get(key, default)
nexom/app/db.py ADDED
@@ -0,0 +1,88 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Iterable
4
+ from sqlite3 import connect, Connection, Cursor, Error
5
+
6
+ from ..core.error import DBMConnectionInvalidError, DBError
7
+
8
+
9
+ class DatabaseManager:
10
+ def __init__(self, db_file: str, auto_commit: bool = True):
11
+ self.db_file: str = db_file
12
+ self.auto_commit: bool = auto_commit
13
+
14
+ self._conn: Connection | None = None
15
+ self._cursor: Cursor | None = None
16
+
17
+ self.start_connection(auto_commit=auto_commit)
18
+ self._init()
19
+
20
+ def _init(self) -> None:
21
+ "for override"
22
+ ...
23
+
24
+ def start_connection(self, auto_commit: bool = True) -> None:
25
+ try:
26
+ self.auto_commit = auto_commit
27
+ self._conn = connect(self.db_file)
28
+ self._cursor = self._conn.cursor()
29
+
30
+ # ---- SQLite safety / performance defaults ----
31
+ # foreign keys are OFF by default in SQLite
32
+ self._cursor.execute("PRAGMA foreign_keys = ON")
33
+ # better concurrency
34
+ self._cursor.execute("PRAGMA journal_mode = WAL")
35
+ # avoid immediate 'database is locked'
36
+ self._cursor.execute("PRAGMA busy_timeout = 3000")
37
+ # reasonable durability vs speed (WAL推奨とセット)
38
+ self._cursor.execute("PRAGMA synchronous = NORMAL")
39
+
40
+ self.commit()
41
+ except Error as e:
42
+ raise DBMConnectionInvalidError(str(e))
43
+
44
+ def rip_connection(self) -> None:
45
+ if self._conn is None:
46
+ raise DBMConnectionInvalidError()
47
+ self._conn.close()
48
+ self._conn = None
49
+ self._cursor = None
50
+
51
+ def commit(self) -> None:
52
+ if self._conn is None or self._cursor is None:
53
+ raise DBMConnectionInvalidError()
54
+ self._conn.commit()
55
+
56
+ # ---- new canonical names ----
57
+
58
+ def execute(self, sql: str, *args: Any) -> list[tuple] | None:
59
+ if self._conn is None or self._cursor is None:
60
+ raise DBMConnectionInvalidError()
61
+
62
+ try:
63
+ self._cursor.execute(sql, tuple(args))
64
+ if self.auto_commit:
65
+ self._conn.commit()
66
+
67
+ if sql.lstrip().upper().startswith("SELECT"):
68
+ return self._cursor.fetchall()
69
+ return None
70
+
71
+ except Error as e:
72
+ self._conn.rollback()
73
+ raise DBError(str(e))
74
+
75
+ def execute_many(self, sql_inserts: Iterable[ list[ tuple[str, tuple] ] ]) -> None:
76
+ if self._conn is None or self._cursor is None:
77
+ raise DBMConnectionInvalidError()
78
+
79
+ try:
80
+ for sql, values in sql_inserts:
81
+ self._cursor.execute(sql, values)
82
+
83
+ if self.auto_commit:
84
+ self._conn.commit()
85
+
86
+ except Error as e:
87
+ self._conn.rollback()
88
+ raise DBError(str(e))