Nexom 1.0.3__py3-none-any.whl → 1.0.5__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.
- nexom/app/__init__.py +1 -1
- nexom/app/auth.py +184 -71
- nexom/app/db.py +34 -6
- nexom/app/path.py +1 -1
- nexom/app/response.py +12 -3
- nexom/app/template.py +21 -17
- nexom/assets/app/__pycache__/__init__.cpython-313.pyc +0 -0
- nexom/assets/app/pages/__pycache__/__init__.cpython-313.pyc +0 -0
- nexom/assets/app/pages/_templates.py +2 -2
- nexom/assets/app/router.py +2 -2
- nexom/assets/app/static/github.png +0 -0
- nexom/assets/app/static/style.css +626 -29
- nexom/assets/app/templates/default.html +7 -3
- nexom/assets/app/templates/document.html +122 -166
- nexom/assets/app/templates/footer.html +3 -3
- nexom/assets/app/templates/header.html +9 -3
- nexom/assets/auth/__pycache__/__init__.cpython-313.pyc +0 -0
- nexom/assets/auth_page/login.html +180 -40
- nexom/assets/auth_page/signup.html +259 -44
- nexom/buildTools/build.py +1 -1
- nexom/core/error.py +125 -32
- nexom/templates/auth.py +89 -23
- {nexom-1.0.3.dist-info → nexom-1.0.5.dist-info}/METADATA +3 -2
- {nexom-1.0.3.dist-info → nexom-1.0.5.dist-info}/RECORD +28 -27
- {nexom-1.0.3.dist-info → nexom-1.0.5.dist-info}/WHEEL +0 -0
- {nexom-1.0.3.dist-info → nexom-1.0.5.dist-info}/entry_points.txt +0 -0
- {nexom-1.0.3.dist-info → nexom-1.0.5.dist-info}/licenses/LICENSE +0 -0
- {nexom-1.0.3.dist-info → nexom-1.0.5.dist-info}/top_level.txt +0 -0
nexom/app/__init__.py
CHANGED
nexom/app/auth.py
CHANGED
|
@@ -8,24 +8,36 @@ import time
|
|
|
8
8
|
import hashlib
|
|
9
9
|
import hmac
|
|
10
10
|
import json
|
|
11
|
+
import sqlite3
|
|
11
12
|
from urllib.request import Request as UrlRequest, urlopen
|
|
12
13
|
from urllib.error import URLError, HTTPError
|
|
13
14
|
|
|
14
15
|
from .request import Request
|
|
15
16
|
from .response import JsonResponse
|
|
16
17
|
from .db import DatabaseManager
|
|
17
|
-
from .path import Path,
|
|
18
|
+
from .path import Path, Router
|
|
18
19
|
from ..core.log import AuthLogger
|
|
19
20
|
|
|
20
21
|
from ..core.error import (
|
|
21
22
|
NexomError,
|
|
22
|
-
AuthMissingFieldError,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
AuthMissingFieldError, # A01
|
|
24
|
+
AuthUserIdAlreadyExistsError, # A02
|
|
25
|
+
AuthInvalidCredentialsError, # A03
|
|
26
|
+
AuthUserDisabledError, # A04
|
|
27
|
+
AuthTokenMissingError, # A05
|
|
28
|
+
AuthTokenInvalidError, # A06
|
|
29
|
+
AuthTokenExpiredError, # A07
|
|
30
|
+
AuthTokenRevokedError, # A08
|
|
31
|
+
AuthServiceUnavailableError, # A09
|
|
32
|
+
_status_for_auth_error,
|
|
33
|
+
|
|
34
|
+
DBError,
|
|
35
|
+
DBMConnectionInvalidError,
|
|
36
|
+
DBOperationalError,
|
|
37
|
+
DBIntegrityError,
|
|
38
|
+
DBProgrammingError,
|
|
26
39
|
)
|
|
27
40
|
|
|
28
|
-
|
|
29
41
|
# --------------------
|
|
30
42
|
# utils
|
|
31
43
|
# --------------------
|
|
@@ -33,20 +45,32 @@ from ..core.error import (
|
|
|
33
45
|
def _now() -> int:
|
|
34
46
|
return int(time.time())
|
|
35
47
|
|
|
48
|
+
|
|
36
49
|
def _rand(nbytes: int = 24) -> str:
|
|
37
50
|
return secrets.token_urlsafe(nbytes)
|
|
38
51
|
|
|
52
|
+
|
|
39
53
|
def _make_salt(nbytes: int = 16) -> str:
|
|
40
54
|
return secrets.token_hex(nbytes)
|
|
41
55
|
|
|
56
|
+
|
|
42
57
|
def _hash_password(password: str, salt_hex: str) -> str:
|
|
43
58
|
salt = bytes.fromhex(salt_hex)
|
|
44
59
|
dk = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, 200_000)
|
|
45
60
|
return dk.hex()
|
|
46
61
|
|
|
62
|
+
|
|
47
63
|
def _token_hash(token: str) -> str:
|
|
48
64
|
return hashlib.sha256(token.encode("utf-8")).hexdigest()
|
|
49
65
|
|
|
66
|
+
|
|
67
|
+
# --------------------
|
|
68
|
+
# variables (internal)
|
|
69
|
+
# --------------------
|
|
70
|
+
|
|
71
|
+
KEY_NAME = "_nxt"
|
|
72
|
+
|
|
73
|
+
|
|
50
74
|
# --------------------
|
|
51
75
|
# models (internal)
|
|
52
76
|
# --------------------
|
|
@@ -61,6 +85,7 @@ class Session:
|
|
|
61
85
|
revoked_at: int | None
|
|
62
86
|
user_agent: str | None
|
|
63
87
|
|
|
88
|
+
|
|
64
89
|
# --------------------
|
|
65
90
|
# AuthService (API only)
|
|
66
91
|
# --------------------
|
|
@@ -70,15 +95,23 @@ class AuthService:
|
|
|
70
95
|
Auth API service (JSON only).
|
|
71
96
|
"""
|
|
72
97
|
|
|
73
|
-
def __init__(
|
|
98
|
+
def __init__(
|
|
99
|
+
self,
|
|
100
|
+
db_path: str,
|
|
101
|
+
log_path: str,
|
|
102
|
+
*,
|
|
103
|
+
ttl_sec: int = 60 * 60 * 24 * 7,
|
|
104
|
+
prefix: str = "",
|
|
105
|
+
) -> None:
|
|
74
106
|
self.dbm = AuthDBM(db_path)
|
|
75
107
|
self.ttl_sec = ttl_sec
|
|
76
108
|
|
|
77
109
|
p = prefix.strip("/")
|
|
110
|
+
|
|
78
111
|
def _p(x: str) -> str:
|
|
79
112
|
return f"{p}/{x}".strip("/") if p else x
|
|
80
113
|
|
|
81
|
-
self.routing =
|
|
114
|
+
self.routing = Router(
|
|
82
115
|
Path(_p("signup"), self.signup, "AuthSignup"),
|
|
83
116
|
Path(_p("login"), self.login, "AuthLogin"),
|
|
84
117
|
Path(_p("logout"), self.logout, "AuthLogout"),
|
|
@@ -92,8 +125,12 @@ class AuthService:
|
|
|
92
125
|
try:
|
|
93
126
|
route = self.routing.get(req.path)
|
|
94
127
|
return route.call_handler(req)
|
|
128
|
+
|
|
95
129
|
except NexomError as e:
|
|
96
|
-
|
|
130
|
+
# error code -> proper HTTP status
|
|
131
|
+
status = _status_for_auth_error(e.code)
|
|
132
|
+
return JsonResponse({"ok": False, "error": e.code}, status=status)
|
|
133
|
+
|
|
97
134
|
except Exception:
|
|
98
135
|
return JsonResponse({"ok": False, "error": "InternalError"}, status=500)
|
|
99
136
|
|
|
@@ -101,38 +138,43 @@ class AuthService:
|
|
|
101
138
|
|
|
102
139
|
def signup(self, request: Request, args: dict[str, Optional[str]]) -> JsonResponse:
|
|
103
140
|
if request.method != "POST":
|
|
104
|
-
return JsonResponse({"ok": False}, status=405)
|
|
141
|
+
return JsonResponse({"ok": False, "error": "MethodNotAllowed"}, status=405)
|
|
105
142
|
|
|
106
143
|
data = request.json() or {}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
)
|
|
144
|
+
user_id = str(data.get("user_id") or "").strip()
|
|
145
|
+
public_name = str(data.get("public_name") or "").strip()
|
|
146
|
+
password = str(data.get("password") or "")
|
|
147
|
+
|
|
148
|
+
self.dbm.signup(user_id=user_id, public_name=public_name, password=password)
|
|
112
149
|
return JsonResponse({"ok": True}, status=201)
|
|
113
150
|
|
|
114
151
|
def login(self, request: Request, args: dict[str, Optional[str]]) -> JsonResponse:
|
|
115
152
|
if request.method != "POST":
|
|
116
|
-
return JsonResponse({"ok": False}, status=405)
|
|
153
|
+
return JsonResponse({"ok": False, "error": "MethodNotAllowed"}, status=405)
|
|
117
154
|
|
|
118
155
|
data = request.json() or {}
|
|
156
|
+
user_id = str(data.get("user_id") or "").strip()
|
|
157
|
+
password = str(data.get("password") or "")
|
|
158
|
+
|
|
119
159
|
sess = self.dbm.login(
|
|
120
|
-
|
|
121
|
-
|
|
160
|
+
user_id,
|
|
161
|
+
password,
|
|
122
162
|
user_agent=request.headers.get("user-agent"),
|
|
123
163
|
ttl_sec=self.ttl_sec,
|
|
124
164
|
)
|
|
125
165
|
|
|
126
|
-
return JsonResponse(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
166
|
+
return JsonResponse(
|
|
167
|
+
{
|
|
168
|
+
"ok": True,
|
|
169
|
+
"user_id": sess.user_id,
|
|
170
|
+
"token": sess.token,
|
|
171
|
+
"expires_at": sess.expires_at,
|
|
172
|
+
}
|
|
173
|
+
)
|
|
132
174
|
|
|
133
175
|
def logout(self, request: Request, args: dict[str, Optional[str]]) -> JsonResponse:
|
|
134
176
|
if request.method != "POST":
|
|
135
|
-
return JsonResponse({"ok": False}, status=405)
|
|
177
|
+
return JsonResponse({"ok": False, "error": "MethodNotAllowed"}, status=405)
|
|
136
178
|
|
|
137
179
|
token = str((request.json() or {}).get("token") or "")
|
|
138
180
|
if token:
|
|
@@ -141,18 +183,22 @@ class AuthService:
|
|
|
141
183
|
|
|
142
184
|
def verify(self, request: Request, args: dict[str, Optional[str]]) -> JsonResponse:
|
|
143
185
|
if request.method != "POST":
|
|
144
|
-
return JsonResponse({"ok": False}, status=405)
|
|
186
|
+
return JsonResponse({"ok": False, "error": "MethodNotAllowed"}, status=405)
|
|
145
187
|
|
|
146
188
|
token = str((request.json() or {}).get("token") or "")
|
|
147
189
|
sess = self.dbm.verify(token)
|
|
148
190
|
if not sess:
|
|
149
|
-
return JsonResponse({"active": False})
|
|
191
|
+
return JsonResponse({"active": False}, status=200)
|
|
192
|
+
|
|
193
|
+
return JsonResponse(
|
|
194
|
+
{
|
|
195
|
+
"active": True,
|
|
196
|
+
"user_id": sess.user_id,
|
|
197
|
+
"expires_at": sess.expires_at,
|
|
198
|
+
},
|
|
199
|
+
status=200,
|
|
200
|
+
)
|
|
150
201
|
|
|
151
|
-
return JsonResponse({
|
|
152
|
-
"active": True,
|
|
153
|
-
"user_id": sess.user_id,
|
|
154
|
-
"expires_at": sess.expires_at,
|
|
155
|
-
})
|
|
156
202
|
|
|
157
203
|
# --------------------
|
|
158
204
|
# AuthClient (App側)
|
|
@@ -170,48 +216,97 @@ class AuthClient:
|
|
|
170
216
|
self.timeout = timeout
|
|
171
217
|
|
|
172
218
|
def _post(self, url: str, body: dict) -> dict:
|
|
173
|
-
payload = json.dumps(body).encode("utf-8")
|
|
219
|
+
payload = json.dumps(body, ensure_ascii=False).encode("utf-8")
|
|
174
220
|
req = UrlRequest(
|
|
175
221
|
url,
|
|
176
222
|
data=payload,
|
|
177
|
-
headers={
|
|
223
|
+
headers={
|
|
224
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
225
|
+
"Accept": "application/json",
|
|
226
|
+
},
|
|
178
227
|
method="POST",
|
|
179
228
|
)
|
|
229
|
+
|
|
180
230
|
try:
|
|
181
231
|
with urlopen(req, timeout=self.timeout) as r:
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
232
|
+
raw = r.read()
|
|
233
|
+
text = raw.decode("utf-8", errors="replace")
|
|
234
|
+
return json.loads(text) if text else {}
|
|
235
|
+
|
|
236
|
+
except HTTPError as e:
|
|
237
|
+
try:
|
|
238
|
+
raw = e.read()
|
|
239
|
+
text = raw.decode("utf-8", errors="replace")
|
|
240
|
+
return json.loads(text) if text else {"ok": False, "error": f"HTTP_{e.code}"}
|
|
241
|
+
except Exception:
|
|
242
|
+
return {"ok": False, "error": f"HTTP_{e.code}"}
|
|
243
|
+
|
|
244
|
+
except (URLError, TimeoutError):
|
|
245
|
+
raise AuthServiceUnavailableError()
|
|
246
|
+
|
|
247
|
+
except json.JSONDecodeError:
|
|
248
|
+
raise AuthServiceUnavailableError()
|
|
249
|
+
|
|
250
|
+
def signup(self, *, user_id: str, public_name: str, password: str) -> None:
|
|
251
|
+
d = self._post(
|
|
252
|
+
self.signup_url,
|
|
253
|
+
{"user_id": user_id, "public_name": public_name, "password": password},
|
|
254
|
+
)
|
|
255
|
+
if d.get("ok"):
|
|
256
|
+
return
|
|
257
|
+
self._raise_from_error_code(str(d.get("error") or ""))
|
|
194
258
|
|
|
195
259
|
def login(self, *, user_id: str, password: str) -> tuple[str, str, int]:
|
|
196
260
|
d = self._post(self.login_url, {"user_id": user_id, "password": password})
|
|
197
|
-
|
|
261
|
+
if not d.get("ok"):
|
|
262
|
+
self._raise_from_error_code(str(d.get("error") or ""))
|
|
263
|
+
|
|
264
|
+
return str(d["token"]), str(d["user_id"]), int(d["expires_at"])
|
|
198
265
|
|
|
199
266
|
def verify_token(self, *, token: str) -> tuple[bool, Optional[str], Optional[int]]:
|
|
200
267
|
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
268
|
|
|
205
|
-
|
|
206
|
-
|
|
269
|
+
if d.get("active") is True:
|
|
270
|
+
return True, str(d["user_id"]), int(d["expires_at"])
|
|
271
|
+
|
|
272
|
+
# active False は正常系扱い
|
|
273
|
+
return False, None, None
|
|
274
|
+
|
|
275
|
+
def logout(self, *, token: str) -> None:
|
|
276
|
+
d = self._post(self.logout_url, {"token": token})
|
|
277
|
+
if d.get("ok"):
|
|
278
|
+
return
|
|
279
|
+
self._raise_from_error_code(str(d.get("error") or ""))
|
|
280
|
+
|
|
281
|
+
def _raise_from_error_code(self, code: str) -> None:
|
|
282
|
+
if code == "A01":
|
|
283
|
+
raise AuthMissingFieldError("unknown")
|
|
284
|
+
if code == "A02":
|
|
285
|
+
raise AuthUserIdAlreadyExistsError()
|
|
286
|
+
if code == "A03":
|
|
287
|
+
raise AuthInvalidCredentialsError()
|
|
288
|
+
if code == "A04":
|
|
289
|
+
raise AuthUserDisabledError()
|
|
290
|
+
if code == "A05":
|
|
291
|
+
raise AuthTokenMissingError()
|
|
292
|
+
if code == "A06":
|
|
293
|
+
raise AuthTokenInvalidError()
|
|
294
|
+
if code == "A07":
|
|
295
|
+
raise AuthTokenExpiredError()
|
|
296
|
+
if code == "A08":
|
|
297
|
+
raise AuthTokenRevokedError()
|
|
298
|
+
if code == "A09":
|
|
299
|
+
raise AuthServiceUnavailableError()
|
|
300
|
+
|
|
301
|
+
# 想定外レスポンス
|
|
302
|
+
raise AuthServiceUnavailableError()
|
|
303
|
+
|
|
207
304
|
|
|
208
305
|
# --------------------
|
|
209
306
|
# DB
|
|
210
307
|
# --------------------
|
|
211
308
|
|
|
212
309
|
class AuthDBM(DatabaseManager):
|
|
213
|
-
|
|
214
|
-
# override
|
|
215
310
|
def _init(self) -> None:
|
|
216
311
|
self.execute_many(
|
|
217
312
|
[
|
|
@@ -241,7 +336,7 @@ class AuthDBM(DatabaseManager):
|
|
|
241
336
|
);
|
|
242
337
|
""",
|
|
243
338
|
(),
|
|
244
|
-
)
|
|
339
|
+
),
|
|
245
340
|
]
|
|
246
341
|
)
|
|
247
342
|
|
|
@@ -254,18 +349,30 @@ class AuthDBM(DatabaseManager):
|
|
|
254
349
|
raise AuthMissingFieldError("password")
|
|
255
350
|
|
|
256
351
|
salt = _make_salt()
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
352
|
+
uid = _rand()
|
|
353
|
+
|
|
354
|
+
try:
|
|
355
|
+
self.execute(
|
|
356
|
+
"INSERT INTO users VALUES(?,?,?,?,?,?,?)",
|
|
357
|
+
uid,
|
|
358
|
+
user_id,
|
|
359
|
+
public_name,
|
|
360
|
+
_hash_password(password, salt),
|
|
361
|
+
salt,
|
|
362
|
+
None,
|
|
363
|
+
1,
|
|
364
|
+
)
|
|
365
|
+
except DBIntegrityError:
|
|
366
|
+
raise AuthUserIdAlreadyExistsError()
|
|
367
|
+
except Exception as e:
|
|
368
|
+
raise AuthServiceUnavailableError()
|
|
267
369
|
|
|
268
370
|
def login(self, user_id: str, password: str, *, user_agent: str | None, ttl_sec: int) -> Session:
|
|
371
|
+
if not user_id:
|
|
372
|
+
raise AuthMissingFieldError("user_id")
|
|
373
|
+
if not password:
|
|
374
|
+
raise AuthMissingFieldError("password")
|
|
375
|
+
|
|
269
376
|
rows = self.execute(
|
|
270
377
|
"SELECT uid, user_id, password_hash, password_salt, is_active FROM users WHERE user_id=?",
|
|
271
378
|
user_id,
|
|
@@ -276,15 +383,17 @@ class AuthDBM(DatabaseManager):
|
|
|
276
383
|
uid, uid_text, pw_hash, salt, active = rows[0]
|
|
277
384
|
if not active:
|
|
278
385
|
raise AuthUserDisabledError()
|
|
279
|
-
|
|
386
|
+
|
|
387
|
+
if not hmac.compare_digest(_hash_password(password, str(salt)), str(pw_hash)):
|
|
280
388
|
raise AuthInvalidCredentialsError()
|
|
281
389
|
|
|
282
390
|
token = _rand()
|
|
283
391
|
exp = _now() + ttl_sec
|
|
392
|
+
sid = _rand()
|
|
284
393
|
|
|
285
394
|
self.execute(
|
|
286
395
|
"INSERT INTO sessions VALUES(?,?,?,?,?,?)",
|
|
287
|
-
|
|
396
|
+
sid,
|
|
288
397
|
uid,
|
|
289
398
|
_token_hash(token),
|
|
290
399
|
exp,
|
|
@@ -292,9 +401,12 @@ class AuthDBM(DatabaseManager):
|
|
|
292
401
|
user_agent,
|
|
293
402
|
)
|
|
294
403
|
|
|
295
|
-
return Session(
|
|
404
|
+
return Session(sid, uid, uid_text, token, exp, None, user_agent)
|
|
296
405
|
|
|
297
406
|
def logout(self, token: str) -> None:
|
|
407
|
+
if not token:
|
|
408
|
+
raise AuthMissingFieldError("token")
|
|
409
|
+
|
|
298
410
|
self.execute(
|
|
299
411
|
"UPDATE sessions SET revoked_at=? WHERE token_hash=?",
|
|
300
412
|
_now(),
|
|
@@ -308,7 +420,8 @@ class AuthDBM(DatabaseManager):
|
|
|
308
420
|
rows = self.execute(
|
|
309
421
|
"""
|
|
310
422
|
SELECT s.sid, s.uid, u.user_id, s.expires_at, s.revoked_at, s.user_agent
|
|
311
|
-
FROM sessions s
|
|
423
|
+
FROM sessions s
|
|
424
|
+
JOIN users u ON u.uid=s.uid
|
|
312
425
|
WHERE s.token_hash=?
|
|
313
426
|
""",
|
|
314
427
|
_token_hash(token),
|
|
@@ -317,7 +430,7 @@ class AuthDBM(DatabaseManager):
|
|
|
317
430
|
return None
|
|
318
431
|
|
|
319
432
|
sid, uid, user_id, exp, rev, ua = rows[0]
|
|
320
|
-
if rev or exp <= _now():
|
|
433
|
+
if rev or int(exp) <= _now():
|
|
321
434
|
return None
|
|
322
435
|
|
|
323
|
-
return Session(sid, uid, user_id, token, exp, None, ua)
|
|
436
|
+
return Session(str(sid), str(uid), str(user_id), str(token), int(exp), None, ua)
|
nexom/app/db.py
CHANGED
|
@@ -1,9 +1,35 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from typing import Any, Iterable
|
|
4
|
-
from sqlite3 import
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
from sqlite3 import (
|
|
5
|
+
connect,
|
|
6
|
+
Connection,
|
|
7
|
+
Cursor,
|
|
8
|
+
|
|
9
|
+
Error,
|
|
10
|
+
OperationalError,
|
|
11
|
+
IntegrityError,
|
|
12
|
+
ProgrammingError
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from ..core.error import (
|
|
16
|
+
DBError,
|
|
17
|
+
|
|
18
|
+
DBMConnectionInvalidError,
|
|
19
|
+
DBOperationalError,
|
|
20
|
+
DBIntegrityError,
|
|
21
|
+
DBProgrammingError
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def _call_error_handler(e: Error):
|
|
25
|
+
if isinstance(e, OperationalError):
|
|
26
|
+
raise DBOperationalError(str(e))
|
|
27
|
+
elif isinstance(e, ProgrammingError):
|
|
28
|
+
raise DBProgrammingError(str(e))
|
|
29
|
+
elif isinstance(e, IntegrityError):
|
|
30
|
+
raise DBIntegrityError(str(e))
|
|
31
|
+
else:
|
|
32
|
+
raise DBMConnectionInvalidError(str(e))
|
|
7
33
|
|
|
8
34
|
|
|
9
35
|
class DatabaseManager:
|
|
@@ -39,7 +65,7 @@ class DatabaseManager:
|
|
|
39
65
|
|
|
40
66
|
self.commit()
|
|
41
67
|
except Error as e:
|
|
42
|
-
|
|
68
|
+
_call_error_handler(e)
|
|
43
69
|
|
|
44
70
|
def rip_connection(self) -> None:
|
|
45
71
|
if self._conn is None:
|
|
@@ -70,7 +96,8 @@ class DatabaseManager:
|
|
|
70
96
|
|
|
71
97
|
except Error as e:
|
|
72
98
|
self._conn.rollback()
|
|
73
|
-
|
|
99
|
+
_call_error_handler(e)
|
|
100
|
+
|
|
74
101
|
|
|
75
102
|
def execute_many(self, sql_inserts: Iterable[ list[ tuple[str, tuple] ] ]) -> None:
|
|
76
103
|
if self._conn is None or self._cursor is None:
|
|
@@ -85,4 +112,5 @@ class DatabaseManager:
|
|
|
85
112
|
|
|
86
113
|
except Error as e:
|
|
87
114
|
self._conn.rollback()
|
|
88
|
-
|
|
115
|
+
_call_error_handler(e)
|
|
116
|
+
|
nexom/app/path.py
CHANGED
nexom/app/response.py
CHANGED
|
@@ -111,7 +111,7 @@ class JsonResponse(Response):
|
|
|
111
111
|
cookie=cookie,
|
|
112
112
|
content_type=content_type,
|
|
113
113
|
charset=charset,
|
|
114
|
-
include_charset=False,
|
|
114
|
+
include_charset=False,
|
|
115
115
|
)
|
|
116
116
|
|
|
117
117
|
class Redirect(Response):
|
|
@@ -119,11 +119,20 @@ class Redirect(Response):
|
|
|
119
119
|
HTTP redirect response (302).
|
|
120
120
|
"""
|
|
121
121
|
|
|
122
|
-
def __init__(
|
|
122
|
+
def __init__(
|
|
123
|
+
self,
|
|
124
|
+
location: str,
|
|
125
|
+
headers: Iterable[Header] | None = None,
|
|
126
|
+
cookie: str | None = None,
|
|
127
|
+
) -> None:
|
|
128
|
+
extra = list(headers) if headers else []
|
|
129
|
+
res_headers = [("Location", location), *extra]
|
|
130
|
+
|
|
123
131
|
super().__init__(
|
|
124
132
|
body=b"",
|
|
125
133
|
status=302,
|
|
126
|
-
headers=
|
|
134
|
+
headers=res_headers,
|
|
135
|
+
cookie=cookie,
|
|
127
136
|
)
|
|
128
137
|
|
|
129
138
|
|
nexom/app/template.py
CHANGED
|
@@ -3,7 +3,6 @@ from __future__ import annotations
|
|
|
3
3
|
import re
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import Any
|
|
7
6
|
|
|
8
7
|
from ..core.object_html_render import HTMLDoc, HTMLDocLib, ObjectHTML
|
|
9
8
|
from ..core.error import TemplateNotFoundError, TemplateInvalidNameError, TemplatesNotDirError
|
|
@@ -26,13 +25,12 @@ class _TemplateAccessor:
|
|
|
26
25
|
templates.layout.base(title="x") -> templates.render("layout.base", title="x")
|
|
27
26
|
"""
|
|
28
27
|
|
|
29
|
-
def __init__(self, templates: ObjectHTMLTemplates, name: str) -> None:
|
|
28
|
+
def __init__(self, templates: "ObjectHTMLTemplates", name: str) -> None:
|
|
30
29
|
self._templates = templates
|
|
31
30
|
self._name = name
|
|
32
31
|
|
|
33
|
-
def __getattr__(self, part: str) -> _TemplateAccessor:
|
|
32
|
+
def __getattr__(self, part: str) -> "_TemplateAccessor":
|
|
34
33
|
if not _SEG_RE.match(part):
|
|
35
|
-
# Attribute access only supports valid segments by design.
|
|
36
34
|
raise AttributeError(part)
|
|
37
35
|
return _TemplateAccessor(self._templates, f"{self._name}.{part}")
|
|
38
36
|
|
|
@@ -54,43 +52,49 @@ class ObjectHTMLTemplates:
|
|
|
54
52
|
templates.a.b(**kwargs) -> render("a.b", **kwargs)
|
|
55
53
|
"""
|
|
56
54
|
|
|
57
|
-
def __init__(self, base_dir: str) -> None:
|
|
55
|
+
def __init__(self, base_dir: str, reload: bool = False) -> None:
|
|
58
56
|
self.base_dir = str(base_dir)
|
|
59
57
|
self._base_path = Path(self.base_dir).resolve()
|
|
58
|
+
self.reload = reload
|
|
60
59
|
|
|
61
60
|
if not self._base_path.exists() or not self._base_path.is_dir():
|
|
62
61
|
raise TemplatesNotDirError(self.base_dir)
|
|
63
62
|
|
|
64
|
-
|
|
65
|
-
for entry in self._scan_templates(self._base_path):
|
|
66
|
-
html_text = entry.path.read_text(encoding="utf-8")
|
|
67
|
-
lib.append(HTMLDoc(entry.name, html_text))
|
|
68
|
-
|
|
69
|
-
self._engine = ObjectHTML(lib=lib)
|
|
63
|
+
self._rebuild_engine()
|
|
70
64
|
|
|
71
65
|
def __getattr__(self, name: str) -> _TemplateAccessor:
|
|
72
|
-
# Called only when normal attribute lookup fails.
|
|
73
66
|
if not _SEG_RE.match(name):
|
|
74
67
|
raise AttributeError(name)
|
|
75
68
|
return _TemplateAccessor(self, name)
|
|
76
69
|
|
|
77
70
|
def render(self, name: str, **kwargs: str) -> str:
|
|
78
|
-
|
|
79
|
-
|
|
71
|
+
if self.reload:
|
|
72
|
+
self._rebuild_engine()
|
|
73
|
+
|
|
74
|
+
if not self._engine.lib.get(name):
|
|
80
75
|
raise TemplateNotFoundError(name)
|
|
76
|
+
|
|
81
77
|
return self._engine.render(name, **kwargs)
|
|
82
78
|
|
|
79
|
+
# -------------------------
|
|
80
|
+
# internal
|
|
81
|
+
# -------------------------
|
|
82
|
+
|
|
83
|
+
def _rebuild_engine(self) -> None:
|
|
84
|
+
lib = HTMLDocLib()
|
|
85
|
+
for entry in self._scan_templates(self._base_path):
|
|
86
|
+
html_text = entry.path.read_text(encoding="utf-8")
|
|
87
|
+
lib.append(HTMLDoc(entry.name, html_text))
|
|
88
|
+
self._engine = ObjectHTML(lib=lib)
|
|
89
|
+
|
|
83
90
|
def _scan_templates(self, root: Path) -> list[TemplateEntry]:
|
|
84
91
|
entries: list[TemplateEntry] = []
|
|
85
|
-
|
|
86
92
|
for path in root.rglob("*.html"):
|
|
87
93
|
if not path.is_file():
|
|
88
94
|
continue
|
|
89
|
-
|
|
90
95
|
rel = path.relative_to(root)
|
|
91
96
|
name = self._path_to_template_name(rel)
|
|
92
97
|
entries.append(TemplateEntry(name=name, path=path))
|
|
93
|
-
|
|
94
98
|
return entries
|
|
95
99
|
|
|
96
100
|
def _path_to_template_name(self, rel_path: Path) -> str:
|
|
Binary file
|
|
Binary file
|
|
@@ -2,6 +2,6 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from nexom.app.template import ObjectHTMLTemplates
|
|
4
4
|
|
|
5
|
-
from __app_name__.config import TEMPLATES_DIR
|
|
5
|
+
from __app_name__.config import TEMPLATES_DIR, RELOAD
|
|
6
6
|
|
|
7
|
-
templates = ObjectHTMLTemplates(base_dir=TEMPLATES_DIR)
|
|
7
|
+
templates = ObjectHTMLTemplates(base_dir=TEMPLATES_DIR, reload=RELOAD)
|
nexom/assets/app/router.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from nexom.app.path import Get, Static,
|
|
3
|
+
from nexom.app.path import Get, Static, Router
|
|
4
4
|
|
|
5
5
|
from .config import APP_DIR
|
|
6
6
|
from .pages import default, document
|
|
7
7
|
|
|
8
|
-
routing =
|
|
8
|
+
routing = Router(
|
|
9
9
|
Get("", default.main, "DefaultPage"),
|
|
10
10
|
Get("doc/", document.main, "DocumentPage"),
|
|
11
11
|
Static("static/", APP_DIR + "/static", "StaticFiles"),
|
|
Binary file
|