jongo 0.2.0__tar.gz → 0.2.2__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.
- {jongo-0.2.0 → jongo-0.2.2}/PKG-INFO +3 -3
- {jongo-0.2.0 → jongo-0.2.2}/README.md +2 -2
- {jongo-0.2.0 → jongo-0.2.2}/jongo/__init__.py +1 -1
- {jongo-0.2.0 → jongo-0.2.2}/jongo/app.py +36 -22
- {jongo-0.2.0 → jongo-0.2.2}/jongo/cli.py +1 -1
- {jongo-0.2.0 → jongo-0.2.2}/jongo/compiler/dom.js +0 -0
- {jongo-0.2.0 → jongo-0.2.2}/jongo/compiler/pyrt.js +124 -42
- {jongo-0.2.0 → jongo-0.2.2}/jongo/compiler/transpile.py +6 -5
- {jongo-0.2.0 → jongo-0.2.2}/jongo/db/connection.py +42 -3
- {jongo-0.2.0 → jongo-0.2.2}/jongo/db/fields.py +11 -7
- {jongo-0.2.0 → jongo-0.2.2}/jongo/db/models.py +28 -17
- {jongo-0.2.0 → jongo-0.2.2}/jongo/db/query.py +32 -3
- {jongo-0.2.0 → jongo-0.2.2}/jongo/http.py +30 -7
- {jongo-0.2.0 → jongo-0.2.2}/jongo/rpc.py +16 -7
- {jongo-0.2.0 → jongo-0.2.2}/jongo/server.py +4 -9
- {jongo-0.2.0 → jongo-0.2.2}/jongo/vdom.py +33 -11
- {jongo-0.2.0 → jongo-0.2.2}/jongo.egg-info/PKG-INFO +3 -3
- {jongo-0.2.0 → jongo-0.2.2}/jongo.egg-info/SOURCES.txt +2 -1
- {jongo-0.2.0 → jongo-0.2.2}/pyproject.toml +1 -1
- jongo-0.2.2/tests/test_stress_fixes_022.py +222 -0
- {jongo-0.2.0 → jongo-0.2.2}/LICENSE +0 -0
- {jongo-0.2.0 → jongo-0.2.2}/jongo/__main__.py +0 -0
- {jongo-0.2.0 → jongo-0.2.2}/jongo/admin.py +0 -0
- {jongo-0.2.0 → jongo-0.2.2}/jongo/auth.py +0 -0
- {jongo-0.2.0 → jongo-0.2.2}/jongo/compiler/__init__.py +0 -0
- {jongo-0.2.0 → jongo-0.2.2}/jongo/compiler/bundle.py +0 -0
- {jongo-0.2.0 → jongo-0.2.2}/jongo/compiler/scope.py +0 -0
- {jongo-0.2.0 → jongo-0.2.2}/jongo/db/__init__.py +0 -0
- {jongo-0.2.0 → jongo-0.2.2}/jongo/db/migrate.py +0 -0
- {jongo-0.2.0 → jongo-0.2.2}/jongo/errors.py +0 -0
- {jongo-0.2.0 → jongo-0.2.2}/jongo/errors_page.py +0 -0
- {jongo-0.2.0 → jongo-0.2.2}/jongo/html.py +0 -0
- {jongo-0.2.0 → jongo-0.2.2}/jongo/log.py +0 -0
- {jongo-0.2.0 → jongo-0.2.2}/jongo/routing.py +0 -0
- {jongo-0.2.0 → jongo-0.2.2}/jongo/scaffold/app.py.tmpl +0 -0
- {jongo-0.2.0 → jongo-0.2.2}/jongo/scaffold/dot-gitignore.tmpl +0 -0
- {jongo-0.2.0 → jongo-0.2.2}/jongo/styles.py +0 -0
- {jongo-0.2.0 → jongo-0.2.2}/jongo/testing.py +0 -0
- {jongo-0.2.0 → jongo-0.2.2}/jongo.egg-info/dependency_links.txt +0 -0
- {jongo-0.2.0 → jongo-0.2.2}/jongo.egg-info/entry_points.txt +0 -0
- {jongo-0.2.0 → jongo-0.2.2}/jongo.egg-info/top_level.txt +0 -0
- {jongo-0.2.0 → jongo-0.2.2}/setup.cfg +0 -0
- {jongo-0.2.0 → jongo-0.2.2}/tests/test_admin.py +0 -0
- {jongo-0.2.0 → jongo-0.2.2}/tests/test_app.py +0 -0
- {jongo-0.2.0 → jongo-0.2.2}/tests/test_compiler.py +0 -0
- {jongo-0.2.0 → jongo-0.2.2}/tests/test_db.py +0 -0
- {jongo-0.2.0 → jongo-0.2.2}/tests/test_stress_fixes.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jongo
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: A full-stack Python web framework: routes, server code and reactive UI in one language.
|
|
5
5
|
Author: Joshua Harty
|
|
6
6
|
License: MIT
|
|
@@ -331,12 +331,12 @@ cd examples/todo && ../../.venv/bin/jongo dev
|
|
|
331
331
|
|
|
332
332
|
## Status
|
|
333
333
|
|
|
334
|
-
Version 0.2 — 0.1 hardened
|
|
334
|
+
Version 0.2 — 0.1 hardened over two stress-audit passes (see `CHANGELOG.md`). Known limits:
|
|
335
335
|
- SQLite only.
|
|
336
336
|
- No WebSockets yet.
|
|
337
337
|
- No classes in browser code.
|
|
338
338
|
- Components re-render their subtree without memoisation.
|
|
339
|
-
- Integers beyond 2⁵³ lose precision in browser code (JavaScript numbers are float64); do big-integer work in a `@server` function.
|
|
340
339
|
- `x__ne=v` / `.exclude(field=v)` also match rows where the column is `NULL` (matching Python's `None != v`, not SQL's three-valued logic).
|
|
340
|
+
- Browser (post-hydration) code inherits JavaScript's value model, so a few things differ from CPython (the server is always correct): integers past 2⁵³ lose precision and `str(2.0)` shows `"2"` (one number type — format explicitly, e.g. `f"{x:.2f}"`); a dict keyed by ints iterates string keys; `len("😀")` counts UTF-16 units; `(1,2) == [1,2]` is true. Do exact numeric/big-integer work in a `@server` function.
|
|
341
341
|
|
|
342
342
|
Bug reports and ideas are welcome.
|
|
@@ -301,12 +301,12 @@ cd examples/todo && ../../.venv/bin/jongo dev
|
|
|
301
301
|
|
|
302
302
|
## Status
|
|
303
303
|
|
|
304
|
-
Version 0.2 — 0.1 hardened
|
|
304
|
+
Version 0.2 — 0.1 hardened over two stress-audit passes (see `CHANGELOG.md`). Known limits:
|
|
305
305
|
- SQLite only.
|
|
306
306
|
- No WebSockets yet.
|
|
307
307
|
- No classes in browser code.
|
|
308
308
|
- Components re-render their subtree without memoisation.
|
|
309
|
-
- Integers beyond 2⁵³ lose precision in browser code (JavaScript numbers are float64); do big-integer work in a `@server` function.
|
|
310
309
|
- `x__ne=v` / `.exclude(field=v)` also match rows where the column is `NULL` (matching Python's `None != v`, not SQL's three-valued logic).
|
|
310
|
+
- Browser (post-hydration) code inherits JavaScript's value model, so a few things differ from CPython (the server is always correct): integers past 2⁵³ lose precision and `str(2.0)` shows `"2"` (one number type — format explicitly, e.g. `f"{x:.2f}"`); a dict keyed by ints iterates string keys; `len("😀")` counts UTF-16 units; `(1,2) == [1,2]` is true. Do exact numeric/big-integer work in a `@server` function.
|
|
311
311
|
|
|
312
312
|
Bug reports and ideas are welcome.
|
|
@@ -18,6 +18,7 @@ import urllib.parse
|
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
|
|
20
20
|
from . import vdom
|
|
21
|
+
from . import log as jlog
|
|
21
22
|
from .errors import CompileError, HTTPError, NotFound
|
|
22
23
|
from .http import (
|
|
23
24
|
CSRF_COOKIE,
|
|
@@ -63,6 +64,7 @@ class Jongo:
|
|
|
63
64
|
login_url: str = "/login",
|
|
64
65
|
session_max_age: int = 14 * 24 * 3600,
|
|
65
66
|
static_url: str = "/static",
|
|
67
|
+
trust_proxy: bool = False,
|
|
66
68
|
):
|
|
67
69
|
self.name = name or "app"
|
|
68
70
|
module = sys.modules.get(name) if name else None
|
|
@@ -79,6 +81,7 @@ class Jongo:
|
|
|
79
81
|
self.login_url = login_url
|
|
80
82
|
self.session_max_age = session_max_age
|
|
81
83
|
self.static_url = static_url.rstrip("/")
|
|
84
|
+
self.trust_proxy = trust_proxy # trust X-Forwarded-Proto only when behind a known proxy
|
|
82
85
|
self.router = Router()
|
|
83
86
|
self.boot_id = secrets.token_hex(8)
|
|
84
87
|
self.before_request_hooks: list = []
|
|
@@ -168,27 +171,38 @@ class Jongo:
|
|
|
168
171
|
|
|
169
172
|
def handle(self, request: Request) -> Response:
|
|
170
173
|
started = time.perf_counter()
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
response
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
174
|
+
with jlog.request_scope():
|
|
175
|
+
try:
|
|
176
|
+
self._load_session(request)
|
|
177
|
+
response = None
|
|
178
|
+
for hook in self.before_request_hooks:
|
|
179
|
+
result = hook(request)
|
|
180
|
+
if result is not None:
|
|
181
|
+
response = self.to_response(result, request)
|
|
182
|
+
break
|
|
183
|
+
if response is None:
|
|
184
|
+
response = self._dispatch(request)
|
|
185
|
+
except HTTPError as exc:
|
|
186
|
+
response = self.http_error(request, exc)
|
|
187
|
+
except Exception as exc:
|
|
188
|
+
response = self.server_error(request, exc)
|
|
189
|
+
try:
|
|
190
|
+
for hook in self.after_request_hooks:
|
|
191
|
+
response = hook(request, response) or response
|
|
192
|
+
self._finish(request, response)
|
|
193
|
+
except Exception as exc:
|
|
194
|
+
# A failing after_request hook or _finish (e.g. a non-serialisable session)
|
|
195
|
+
# must still yield a proper 500 instead of escaping to the WSGI server.
|
|
196
|
+
response = self.server_error(request, exc)
|
|
197
|
+
try:
|
|
198
|
+
self._finish(request, response)
|
|
199
|
+
except Exception:
|
|
200
|
+
jlog.get_logger("http").error("failed to finalise error response", exc_info=True)
|
|
201
|
+
if self.dev and not (request.path.startswith("/_jongo/app") or request.path == "/_jongo/live"):
|
|
202
|
+
elapsed = (time.perf_counter() - started) * 1000
|
|
203
|
+
route_name = getattr(getattr(request, "route", None), "name", None)
|
|
204
|
+
jlog.request_done(request.method, request.full_path, response.status, elapsed, route=route_name)
|
|
205
|
+
return response
|
|
192
206
|
|
|
193
207
|
def _dispatch(self, request: Request) -> Response:
|
|
194
208
|
route, params = self.router.match(request.method, request.path)
|
|
@@ -381,7 +395,7 @@ class Jongo:
|
|
|
381
395
|
return Response(http_error_page(exc), exc.status, headers)
|
|
382
396
|
|
|
383
397
|
def server_error(self, request: Request, exc: Exception) -> Response:
|
|
384
|
-
|
|
398
|
+
jlog.get_logger("http").error("error handling %s %s", request.method, request.full_path, exc_info=exc)
|
|
385
399
|
if request.path.startswith("/_jongo/rpc/"):
|
|
386
400
|
error = {"type": type(exc).__name__, "message": str(exc) if self.dev else "Server error"}
|
|
387
401
|
if self.dev:
|
|
@@ -177,7 +177,7 @@ def cmd_build(args):
|
|
|
177
177
|
|
|
178
178
|
def main(argv=None):
|
|
179
179
|
parser = argparse.ArgumentParser(prog="jongo", description="Full-stack Python web framework")
|
|
180
|
-
parser.add_argument("--version", action="version", version="jongo 0.2.
|
|
180
|
+
parser.add_argument("--version", action="version", version="jongo 0.2.2")
|
|
181
181
|
sub = parser.add_subparsers(dest="command", metavar="command")
|
|
182
182
|
|
|
183
183
|
def add(name, fn, help_text, app_arg=True):
|
|
Binary file
|
|
@@ -121,12 +121,23 @@ function $cmp(a, b) {
|
|
|
121
121
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
// Python hashes True as 1 and False as 0, so a bool used as a set element or dict key
|
|
125
|
+
// is the same as the int. JS keeps them distinct (Set) / stringifies to "true" (object),
|
|
126
|
+
// so normalise bools to numbers wherever a value is used as a key/member.
|
|
127
|
+
function $key(x) {
|
|
128
|
+
return typeof x === "boolean" ? +x : x;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function $set(items) {
|
|
132
|
+
return new Set(items.map($key));
|
|
133
|
+
}
|
|
134
|
+
|
|
124
135
|
function $in(x, c) {
|
|
125
136
|
if (typeof c === "string") return c.includes(x);
|
|
126
137
|
if (Array.isArray(c)) return c.some((y) => $eq(x, y));
|
|
127
|
-
if (c instanceof Set || c instanceof Map) return c.has(x);
|
|
128
|
-
if ($isPlain(c)) return $hasOwn(c, x);
|
|
129
|
-
if (c !== null && typeof c === "object") return x in c;
|
|
138
|
+
if (c instanceof Set || c instanceof Map) return c.has($key(x));
|
|
139
|
+
if ($isPlain(c)) return $hasOwn(c, $key(x));
|
|
140
|
+
if (c !== null && typeof c === "object") return $key(x) in c;
|
|
130
141
|
throw $b.TypeError(`argument of type '${$typename(c)}' is not iterable`);
|
|
131
142
|
}
|
|
132
143
|
|
|
@@ -144,6 +155,8 @@ function $iadd(a, b) {
|
|
|
144
155
|
}
|
|
145
156
|
|
|
146
157
|
function $mul(a, b) {
|
|
158
|
+
if (typeof a === "boolean") a = +a; // Python: True*[1]==[1], False*[1]==[]
|
|
159
|
+
if (typeof b === "boolean") b = +b;
|
|
147
160
|
if (typeof a === "number" && typeof b !== "number") [a, b] = [b, a];
|
|
148
161
|
if (typeof a === "string") return b > 0 ? a.repeat(b) : "";
|
|
149
162
|
if (Array.isArray(a)) {
|
|
@@ -165,10 +178,18 @@ function $mod(a, b) {
|
|
|
165
178
|
return ((a % b) + b) % b;
|
|
166
179
|
}
|
|
167
180
|
|
|
181
|
+
// JS bitwise operators are 32-bit signed; use BigInt so ints up to the float64 range
|
|
182
|
+
// stay correct (1<<40, 0xFFFFFFFF & …) instead of wrapping/going negative.
|
|
183
|
+
function $bigint(x, op) {
|
|
184
|
+
if (typeof x === "boolean") return x ? 1n : 0n;
|
|
185
|
+
if (typeof x === "number" && Number.isInteger(x)) return BigInt(x);
|
|
186
|
+
throw $b.TypeError(`unsupported operand type(s) for ${op}: '${$typename(x)}'`);
|
|
187
|
+
}
|
|
188
|
+
|
|
168
189
|
function $bor(a, b) {
|
|
169
190
|
if ($isPlain(a) && $isPlain(b)) return { ...a, ...b };
|
|
170
191
|
if (a instanceof Set && b instanceof Set) return new Set([...a, ...b]);
|
|
171
|
-
return a | b;
|
|
192
|
+
return Number($bigint(a, "|") | $bigint(b, "|"));
|
|
172
193
|
}
|
|
173
194
|
|
|
174
195
|
function $sub(a, b) {
|
|
@@ -178,14 +199,22 @@ function $sub(a, b) {
|
|
|
178
199
|
|
|
179
200
|
function $band(a, b) {
|
|
180
201
|
if (a instanceof Set && b instanceof Set) return new Set([...a].filter((x) => b.has(x)));
|
|
181
|
-
return a & b;
|
|
202
|
+
return Number($bigint(a, "&") & $bigint(b, "&"));
|
|
182
203
|
}
|
|
183
204
|
|
|
184
205
|
function $bxor(a, b) {
|
|
185
206
|
if (a instanceof Set && b instanceof Set) {
|
|
186
207
|
return new Set([...a].filter((x) => !b.has(x)).concat([...b].filter((x) => !a.has(x))));
|
|
187
208
|
}
|
|
188
|
-
return a ^ b;
|
|
209
|
+
return Number($bigint(a, "^") ^ $bigint(b, "^"));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function $lshift(a, b) {
|
|
213
|
+
return Number($bigint(a, "<<") << $bigint(b, "<<"));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function $rshift(a, b) {
|
|
217
|
+
return Number($bigint(a, ">>") >> $bigint(b, ">>"));
|
|
189
218
|
}
|
|
190
219
|
|
|
191
220
|
// ---- iteration, indexing, attributes ------------------------------------------
|
|
@@ -219,8 +248,9 @@ function $gi(o, k) {
|
|
|
219
248
|
return o[i];
|
|
220
249
|
}
|
|
221
250
|
} else if ($isPlain(o)) {
|
|
222
|
-
|
|
223
|
-
|
|
251
|
+
const nk = $key(k);
|
|
252
|
+
if (!$hasOwn(o, nk)) throw $b.KeyError($repr(k));
|
|
253
|
+
return o[nk];
|
|
224
254
|
} else if (o instanceof Map) {
|
|
225
255
|
if (!o.has(k)) throw $b.KeyError($repr(k));
|
|
226
256
|
return o.get(k);
|
|
@@ -382,6 +412,7 @@ function $fmt(x, spec) {
|
|
|
382
412
|
const p = prec === undefined ? undefined : +prec;
|
|
383
413
|
let body;
|
|
384
414
|
let signStr = "";
|
|
415
|
+
let prefix = "";
|
|
385
416
|
if (typeof x === "number") {
|
|
386
417
|
const n = Math.abs(x);
|
|
387
418
|
switch (type) {
|
|
@@ -393,36 +424,55 @@ function $fmt(x, spec) {
|
|
|
393
424
|
case "X": body = Math.trunc(n).toString(16).toUpperCase(); break;
|
|
394
425
|
case "b": body = Math.trunc(n).toString(2); break;
|
|
395
426
|
case "o": body = Math.trunc(n).toString(8); break;
|
|
396
|
-
case "
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
if (type === "E") body = body.toUpperCase();
|
|
400
|
-
if (alt) {
|
|
401
|
-
const prefix = { x: "0x", X: "0X", o: "0o", b: "0b" }[type];
|
|
402
|
-
if (prefix) body = prefix + body;
|
|
427
|
+
case "c": body = String.fromCodePoint(Math.trunc(x)); break;
|
|
428
|
+
case "g": case "G": body = $gformat(n, p === undefined ? 6 : p || 1); break;
|
|
429
|
+
default: body = p === undefined ? String(n) : $gformat(n, p || 1);
|
|
403
430
|
}
|
|
431
|
+
if (type === "E" || type === "G") body = body.toUpperCase();
|
|
432
|
+
if (alt) prefix = { x: "0x", X: "0X", o: "0o", b: "0b" }[type] || "";
|
|
404
433
|
if (group) body = body.replace(/^(\d+)/, (d) => d.replace(/\B(?=(\d{3})+(?!\d))/g, group));
|
|
405
434
|
if (type === "%") body += "%";
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
435
|
+
if (type === "c") {
|
|
436
|
+
align = align || "<"; // a char is aligned like a string and carries no sign
|
|
437
|
+
} else {
|
|
438
|
+
signStr = x < 0 ? "-" : sign === "+" ? "+" : sign === " " ? " " : "";
|
|
439
|
+
if (zero && !align) {
|
|
440
|
+
fill = "0";
|
|
441
|
+
align = "=";
|
|
442
|
+
}
|
|
443
|
+
align = align || ">";
|
|
410
444
|
}
|
|
411
|
-
align = align || ">";
|
|
412
445
|
} else {
|
|
413
446
|
body = $str(x);
|
|
414
447
|
if (p !== undefined) body = body.slice(0, p);
|
|
415
448
|
align = align || "<";
|
|
416
449
|
}
|
|
417
450
|
const w = width === undefined ? 0 : +width;
|
|
418
|
-
const pad = Math.max(0, w - body.length - signStr.length);
|
|
419
|
-
|
|
420
|
-
|
|
451
|
+
const pad = Math.max(0, w - body.length - signStr.length - prefix.length);
|
|
452
|
+
// "=" fills between the sign/prefix and the digits, so f"{255:#06x}" -> "0x00ff".
|
|
453
|
+
if (align === "=") return signStr + prefix + fill.repeat(pad) + body;
|
|
454
|
+
const text = signStr + prefix + body;
|
|
421
455
|
if (align === "<") return text + fill.repeat(pad);
|
|
422
456
|
if (align === "^") return fill.repeat(Math.floor(pad / 2)) + text + fill.repeat(Math.ceil(pad / 2));
|
|
423
457
|
return fill.repeat(pad) + text;
|
|
424
458
|
}
|
|
425
459
|
|
|
460
|
+
// Python's general ("g") float format: shortest of fixed/exponential at `pr` significant
|
|
461
|
+
// digits, trailing zeros stripped, exponent shown when < -4 or >= pr.
|
|
462
|
+
function $gformat(n, pr) {
|
|
463
|
+
if (n === 0) return "0";
|
|
464
|
+
const exp = parseInt(n.toExponential().split("e")[1], 10);
|
|
465
|
+
if (exp < -4 || exp >= pr) {
|
|
466
|
+
let [mant, e] = n.toExponential(pr - 1).split("e");
|
|
467
|
+
if (mant.indexOf(".") >= 0) mant = mant.replace(/0+$/, "").replace(/\.$/, "");
|
|
468
|
+
const en = parseInt(e, 10);
|
|
469
|
+
return mant + "e" + (en < 0 ? "-" : "+") + String(Math.abs(en)).padStart(2, "0");
|
|
470
|
+
}
|
|
471
|
+
let s = n.toFixed(Math.max(0, pr - 1 - exp));
|
|
472
|
+
if (s.indexOf(".") >= 0) s = s.replace(/0+$/, "").replace(/\.$/, "");
|
|
473
|
+
return s;
|
|
474
|
+
}
|
|
475
|
+
|
|
426
476
|
function $percentFormat(template, values) {
|
|
427
477
|
const positional = Array.isArray(values) ? values : [values];
|
|
428
478
|
let index = 0;
|
|
@@ -513,17 +563,41 @@ function $minmax(which) {
|
|
|
513
563
|
}
|
|
514
564
|
|
|
515
565
|
function $toInt(x, base) {
|
|
516
|
-
if (typeof x === "string") {
|
|
517
|
-
const text = x.trim().replace(/_/g, "");
|
|
518
|
-
const n = parseInt(text, base || 10);
|
|
519
|
-
if (Number.isNaN(n) || !/^[-+]?[0-9a-zA-Z]+$/.test(text)) {
|
|
520
|
-
throw $b.ValueError(`invalid literal for int() with base ${base || 10}: ${$repr(x)}`);
|
|
521
|
-
}
|
|
522
|
-
return n;
|
|
523
|
-
}
|
|
524
566
|
if (typeof x === "boolean") return x ? 1 : 0;
|
|
525
567
|
if (x === undefined) return 0;
|
|
526
|
-
return Math.trunc(x);
|
|
568
|
+
if (typeof x === "number") return Math.trunc(x);
|
|
569
|
+
if (typeof x !== "string") {
|
|
570
|
+
throw $b.TypeError(`int() argument must be a string or a number, not '${$typename(x)}'`);
|
|
571
|
+
}
|
|
572
|
+
const b = base === undefined ? 10 : base;
|
|
573
|
+
const fail = () => {
|
|
574
|
+
throw $b.ValueError(`invalid literal for int() with base ${b}: ${$repr(x)}`);
|
|
575
|
+
};
|
|
576
|
+
let s = x.trim();
|
|
577
|
+
let sign = 1;
|
|
578
|
+
if (s[0] === "+" || s[0] === "-") {
|
|
579
|
+
if (s[0] === "-") sign = -1;
|
|
580
|
+
s = s.slice(1);
|
|
581
|
+
}
|
|
582
|
+
let radix = b;
|
|
583
|
+
const low = s.toLowerCase();
|
|
584
|
+
if (b === 0) {
|
|
585
|
+
if (low.startsWith("0x")) (radix = 16), (s = s.slice(2));
|
|
586
|
+
else if (low.startsWith("0o")) (radix = 8), (s = s.slice(2));
|
|
587
|
+
else if (low.startsWith("0b")) (radix = 2), (s = s.slice(2));
|
|
588
|
+
else radix = 10;
|
|
589
|
+
} else if (b === 16 && low.startsWith("0x")) s = s.slice(2);
|
|
590
|
+
else if (b === 8 && low.startsWith("0o")) s = s.slice(2);
|
|
591
|
+
else if (b === 2 && low.startsWith("0b")) s = s.slice(2);
|
|
592
|
+
if (radix < 2 || radix > 36) throw $b.ValueError("int() base must be >= 2 and <= 36, or 0");
|
|
593
|
+
if (/^_|_$|__/.test(s)) fail(); // underscores only as single separators between digits
|
|
594
|
+
s = s.replace(/_/g, "");
|
|
595
|
+
if (s === "") fail();
|
|
596
|
+
const digits = "0123456789abcdefghijklmnopqrstuvwxyz".slice(0, radix);
|
|
597
|
+
if (!new RegExp(`^[${digits}]+$`, "i").test(s)) fail(); // reject trailing garbage like "1a"/"42px"
|
|
598
|
+
const n = parseInt(s, radix);
|
|
599
|
+
if (Number.isNaN(n)) fail();
|
|
600
|
+
return sign * n;
|
|
527
601
|
}
|
|
528
602
|
|
|
529
603
|
function $round(x, n) {
|
|
@@ -567,10 +641,16 @@ Object.assign($b, {
|
|
|
567
641
|
str: (x = "") => $str(x),
|
|
568
642
|
int: $toInt,
|
|
569
643
|
float(x = 0) {
|
|
570
|
-
|
|
571
|
-
if (
|
|
572
|
-
|
|
573
|
-
|
|
644
|
+
if (typeof x === "boolean") return x ? 1 : 0;
|
|
645
|
+
if (typeof x === "number") return x;
|
|
646
|
+
const s = String(x).trim();
|
|
647
|
+
const body = s.replace(/^[+-]/, "");
|
|
648
|
+
const low = body.toLowerCase();
|
|
649
|
+
if (low === "inf" || low === "infinity") return s[0] === "-" ? -Infinity : Infinity;
|
|
650
|
+
if (low === "nan") return NaN;
|
|
651
|
+
if (/^_|_$|__/.test(body)) throw $b.ValueError(`could not convert string to float: ${$repr(x)}`);
|
|
652
|
+
const n = Number(s.replace(/_/g, "")); // Python allows single "_" digit separators
|
|
653
|
+
if (Number.isNaN(n)) throw $b.ValueError(`could not convert string to float: ${$repr(x)}`);
|
|
574
654
|
return n;
|
|
575
655
|
},
|
|
576
656
|
bool: (x) => $t(x),
|
|
@@ -625,7 +705,8 @@ Object.assign($b, {
|
|
|
625
705
|
if (Array.isArray(T)) return T.some((t) => $b.isinstance(x, t));
|
|
626
706
|
switch (T) {
|
|
627
707
|
case $b.str: return typeof x === "string";
|
|
628
|
-
|
|
708
|
+
// bool is a subclass of int in Python (isinstance(True, int) is True).
|
|
709
|
+
case $b.int: return typeof x === "boolean" || (typeof x === "number" && Number.isInteger(x));
|
|
629
710
|
case $b.float: return typeof x === "number";
|
|
630
711
|
case $b.bool: return typeof x === "boolean";
|
|
631
712
|
case $b.list: case $b.tuple: return Array.isArray(x);
|
|
@@ -795,19 +876,19 @@ const $LIST = {
|
|
|
795
876
|
};
|
|
796
877
|
|
|
797
878
|
const $DICT = {
|
|
798
|
-
get: (d, [k, dflt = null]) => ($hasOwn(d, k) ? d[k] : dflt),
|
|
879
|
+
get: (d, [k, dflt = null]) => ($hasOwn(d, $key(k)) ? d[$key(k)] : dflt),
|
|
799
880
|
keys: (d) => Object.keys(d),
|
|
800
881
|
values: (d) => Object.values(d),
|
|
801
882
|
items: (d) => Object.entries(d),
|
|
802
883
|
pop(d, args) {
|
|
803
|
-
const k = args[0];
|
|
884
|
+
const k = $key(args[0]);
|
|
804
885
|
if ($hasOwn(d, k)) {
|
|
805
886
|
const v = d[k];
|
|
806
887
|
delete d[k];
|
|
807
888
|
return v;
|
|
808
889
|
}
|
|
809
890
|
if (args.length > 1) return args[1];
|
|
810
|
-
throw $b.KeyError($repr(
|
|
891
|
+
throw $b.KeyError($repr(args[0]));
|
|
811
892
|
},
|
|
812
893
|
update(d, [o], kw) {
|
|
813
894
|
if (o !== undefined && o !== null) {
|
|
@@ -817,8 +898,9 @@ const $DICT = {
|
|
|
817
898
|
Object.assign(d, kw);
|
|
818
899
|
},
|
|
819
900
|
setdefault(d, [k, v = null]) {
|
|
820
|
-
|
|
821
|
-
|
|
901
|
+
const nk = $key(k);
|
|
902
|
+
if (!$hasOwn(d, nk)) d[nk] = v;
|
|
903
|
+
return d[nk];
|
|
822
904
|
},
|
|
823
905
|
copy: (d) => ({ ...d }),
|
|
824
906
|
clear(d) { for (const k of Object.keys(d)) delete d[k]; },
|
|
@@ -574,13 +574,14 @@ class FunctionTranspiler:
|
|
|
574
574
|
return f"({left} - {right})"
|
|
575
575
|
return f"$sub({left}, {right})" # set difference when both are sets
|
|
576
576
|
simple = {
|
|
577
|
-
ast.Div: "/", ast.Pow: "**",
|
|
577
|
+
ast.Div: "/", ast.Pow: "**",
|
|
578
578
|
}
|
|
579
579
|
if kind in simple:
|
|
580
580
|
return f"({left} {simple[kind]} {right})"
|
|
581
581
|
helpers = {
|
|
582
582
|
ast.FloorDiv: "$floordiv", ast.Mod: "$mod", ast.BitOr: "$bor",
|
|
583
583
|
ast.BitAnd: "$band", ast.BitXor: "$bxor", # set intersection / symmetric difference
|
|
584
|
+
ast.LShift: "$lshift", ast.RShift: "$rshift", # BigInt-backed, not 32-bit JS shifts
|
|
584
585
|
}
|
|
585
586
|
if kind in helpers:
|
|
586
587
|
return f"{helpers[kind]}({left}, {right})"
|
|
@@ -646,7 +647,7 @@ class FunctionTranspiler:
|
|
|
646
647
|
x_Tuple = x_List
|
|
647
648
|
|
|
648
649
|
def x_Set(self, node):
|
|
649
|
-
return f"
|
|
650
|
+
return f"$set([{self._elements(node.elts)}])" # normalises bool/int members (True == 1)
|
|
650
651
|
|
|
651
652
|
def x_Dict(self, node):
|
|
652
653
|
items = []
|
|
@@ -656,7 +657,7 @@ class FunctionTranspiler:
|
|
|
656
657
|
elif isinstance(key, ast.Constant) and isinstance(key.value, str):
|
|
657
658
|
items.append(f"{js_string(key.value)}: {self.expr(value)}")
|
|
658
659
|
else:
|
|
659
|
-
items.append(f"[{self.expr(key)}]: {self.expr(value)}")
|
|
660
|
+
items.append(f"[$key({self.expr(key)})]: {self.expr(value)}") # True/False key == 1/0
|
|
660
661
|
return "({" + ", ".join(items) + "})"
|
|
661
662
|
|
|
662
663
|
def comprehension(self, node, kind) -> str:
|
|
@@ -679,9 +680,9 @@ class FunctionTranspiler:
|
|
|
679
680
|
for cond in gen.ifs:
|
|
680
681
|
code.append(f"if (!{self.test(cond)}) continue;")
|
|
681
682
|
if kind == "dict":
|
|
682
|
-
code.append(f"$r[{self.expr(node.key)}] = {self.expr(node.value)};")
|
|
683
|
+
code.append(f"$r[$key({self.expr(node.key)})] = {self.expr(node.value)};")
|
|
683
684
|
elif kind == "set":
|
|
684
|
-
code.append(f"$r.add({self.expr(node.elt)});")
|
|
685
|
+
code.append(f"$r.add($key({self.expr(node.elt)}));")
|
|
685
686
|
else:
|
|
686
687
|
code.append(f"$r.push({self.expr(node.elt)});")
|
|
687
688
|
code.append("}" * len(generators))
|
|
@@ -5,11 +5,14 @@ from __future__ import annotations
|
|
|
5
5
|
import itertools
|
|
6
6
|
import os
|
|
7
7
|
import sqlite3
|
|
8
|
+
import time
|
|
8
9
|
import threading
|
|
9
10
|
import weakref
|
|
10
11
|
from contextlib import ContextDecorator, contextmanager
|
|
11
12
|
from typing import Any, Callable, Iterator, Sequence
|
|
12
13
|
|
|
14
|
+
from .. import log as _jlog
|
|
15
|
+
|
|
13
16
|
MEMORY = ":memory:"
|
|
14
17
|
DEFAULT_DATABASE = "db.sqlite3"
|
|
15
18
|
ENV_VAR = "JONGO_DATABASE"
|
|
@@ -24,8 +27,34 @@ _memory_connection: _Connection | None = None
|
|
|
24
27
|
_savepoint_ids = itertools.count(1)
|
|
25
28
|
|
|
26
29
|
|
|
30
|
+
_WRITE = frozenset(("INSERT", "UPDATE", "DELETE", "REPLACE"))
|
|
31
|
+
|
|
32
|
+
|
|
27
33
|
class _Connection(sqlite3.Connection):
|
|
28
|
-
"""A sqlite3 connection that can be weakly referenced (so we can track it).
|
|
34
|
+
"""A sqlite3 connection that can be weakly referenced (so we can track it).
|
|
35
|
+
|
|
36
|
+
Its ``execute`` is the single choke point for data-modifying statements: it
|
|
37
|
+
times and logs INSERT/UPDATE/DELETE via ``jongo.log`` — including the
|
|
38
|
+
row-level writes ``Model.save`` issues straight on the connection, which
|
|
39
|
+
would otherwise bypass the module-level ``execute``. Reads are logged in
|
|
40
|
+
``fetch`` (which knows the row count); transaction-control and PRAGMA
|
|
41
|
+
statements are left silent. When SQL logging is off (production) this adds
|
|
42
|
+
only one ``isEnabledFor`` check per statement.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def execute(self, sql, parameters=()): # type: ignore[override]
|
|
46
|
+
if not _jlog.sql_enabled():
|
|
47
|
+
return super().execute(sql, parameters)
|
|
48
|
+
head = sql.lstrip()[:12].split(None, 1)[0].upper() if sql.strip() else ""
|
|
49
|
+
if head not in _WRITE:
|
|
50
|
+
return super().execute(sql, parameters) # reads -> fetch(); control -> silent
|
|
51
|
+
t0 = time.perf_counter()
|
|
52
|
+
cursor = super().execute(sql, parameters)
|
|
53
|
+
ms = (time.perf_counter() - t0) * 1000
|
|
54
|
+
rows = cursor.rowcount if cursor.rowcount is not None and cursor.rowcount >= 0 else None
|
|
55
|
+
_jlog.sql(sql, ms, rows)
|
|
56
|
+
_jlog.record_sql(ms)
|
|
57
|
+
return cursor
|
|
29
58
|
|
|
30
59
|
|
|
31
60
|
def _parse_url(url: str | os.PathLike) -> str:
|
|
@@ -140,7 +169,10 @@ def locked() -> Iterator[sqlite3.Connection]:
|
|
|
140
169
|
|
|
141
170
|
|
|
142
171
|
def execute(sql: str, params: Sequence[Any] | dict = ()) -> sqlite3.Cursor:
|
|
143
|
-
"""Execute one SQL statement and return the cursor.
|
|
172
|
+
"""Execute one SQL statement and return the cursor.
|
|
173
|
+
|
|
174
|
+
Data-modifying statements are timed and logged by ``_Connection.execute``.
|
|
175
|
+
"""
|
|
144
176
|
with locked() as conn:
|
|
145
177
|
return conn.execute(sql, params)
|
|
146
178
|
|
|
@@ -148,7 +180,14 @@ def execute(sql: str, params: Sequence[Any] | dict = ()) -> sqlite3.Cursor:
|
|
|
148
180
|
def fetch(sql: str, params: Sequence[Any] | dict = ()) -> list[sqlite3.Row]:
|
|
149
181
|
"""Execute a query and return all rows (fetched while holding the lock)."""
|
|
150
182
|
with locked() as conn:
|
|
151
|
-
|
|
183
|
+
if not _jlog.sql_enabled():
|
|
184
|
+
return conn.execute(sql, params).fetchall()
|
|
185
|
+
t0 = time.perf_counter()
|
|
186
|
+
rows = conn.execute(sql, params).fetchall()
|
|
187
|
+
ms = (time.perf_counter() - t0) * 1000
|
|
188
|
+
_jlog.sql(sql, ms, len(rows))
|
|
189
|
+
_jlog.record_sql(ms)
|
|
190
|
+
return rows
|
|
152
191
|
|
|
153
192
|
|
|
154
193
|
def query(sql: str, params: Sequence[Any] | dict = ()) -> list[dict[str, Any]]:
|
|
@@ -356,12 +356,15 @@ class DateTime(Field):
|
|
|
356
356
|
return super().required and not (self.auto_now or self.auto_now_add)
|
|
357
357
|
|
|
358
358
|
def to_db(self, value: Any) -> Any:
|
|
359
|
-
# Normalise to UTC
|
|
360
|
-
#
|
|
359
|
+
# Normalise aware datetimes to UTC (so mixed offsets sort/range-compare chronologically)
|
|
360
|
+
# while leaving naive datetimes naive, so each round-trips to an equal value of the same
|
|
361
|
+
# kind. (Mixing naive and aware values in one column is the only case left unsorted.)
|
|
361
362
|
if isinstance(value, datetime):
|
|
362
|
-
|
|
363
|
+
if value.tzinfo is not None:
|
|
364
|
+
value = value.astimezone(timezone.utc)
|
|
365
|
+
return value.isoformat(timespec="microseconds")
|
|
363
366
|
if isinstance(value, date):
|
|
364
|
-
return datetime.combine(value, time()
|
|
367
|
+
return datetime.combine(value, time()).isoformat(timespec="microseconds")
|
|
365
368
|
return value
|
|
366
369
|
|
|
367
370
|
def to_python(self, value: Any) -> Any:
|
|
@@ -397,7 +400,7 @@ class Date(Field):
|
|
|
397
400
|
|
|
398
401
|
def to_db(self, value: Any) -> Any:
|
|
399
402
|
if isinstance(value, datetime):
|
|
400
|
-
return value.date().isoformat()
|
|
403
|
+
return _to_utc(value).date().isoformat() # UTC date, consistent with DateTime
|
|
401
404
|
if isinstance(value, date):
|
|
402
405
|
return value.isoformat()
|
|
403
406
|
return value
|
|
@@ -416,14 +419,15 @@ class Date(Field):
|
|
|
416
419
|
if value is None or _is_blank_string(value):
|
|
417
420
|
return None
|
|
418
421
|
if isinstance(value, datetime):
|
|
419
|
-
return value.date()
|
|
422
|
+
return _to_utc(value).date() if value.tzinfo is not None else value.date()
|
|
420
423
|
if isinstance(value, date):
|
|
421
424
|
return value
|
|
422
425
|
if isinstance(value, str):
|
|
423
426
|
text = value.strip()
|
|
424
427
|
try:
|
|
425
428
|
if len(text) > 10:
|
|
426
|
-
|
|
429
|
+
parsed = _parse_datetime_string(text)
|
|
430
|
+
return (_to_utc(parsed).date() if parsed.tzinfo is not None else parsed.date())
|
|
427
431
|
return date.fromisoformat(text)
|
|
428
432
|
except ValueError:
|
|
429
433
|
pass
|