velocity-python 0.2.4__py3-none-any.whl → 0.2.6__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.
- velocity/__init__.py +1 -1
- velocity/aws/authz.py +23 -2
- velocity/db/core/async_support.py +3 -13
- velocity/db/core/transaction.py +74 -18
- {velocity_python-0.2.4.dist-info → velocity_python-0.2.6.dist-info}/METADATA +1 -1
- {velocity_python-0.2.4.dist-info → velocity_python-0.2.6.dist-info}/RECORD +10 -10
- {velocity_python-0.2.4.dist-info → velocity_python-0.2.6.dist-info}/WHEEL +1 -1
- {velocity_python-0.2.4.dist-info → velocity_python-0.2.6.dist-info}/entry_points.txt +0 -0
- {velocity_python-0.2.4.dist-info → velocity_python-0.2.6.dist-info}/licenses/LICENSE +0 -0
- {velocity_python-0.2.4.dist-info → velocity_python-0.2.6.dist-info}/top_level.txt +0 -0
velocity/__init__.py
CHANGED
velocity/aws/authz.py
CHANGED
|
@@ -80,6 +80,7 @@ from __future__ import annotations
|
|
|
80
80
|
|
|
81
81
|
import logging
|
|
82
82
|
import re
|
|
83
|
+
import sys
|
|
83
84
|
from dataclasses import dataclass, field, replace
|
|
84
85
|
from fnmatch import fnmatchcase
|
|
85
86
|
from typing import Any, Callable, Optional
|
|
@@ -379,22 +380,42 @@ BUILTIN_RULES = (permit("data.ordinary_tables", DATA_ACCESS, when=_ordinary_tabl
|
|
|
379
380
|
|
|
380
381
|
_REGISTERED = {}
|
|
381
382
|
_REGISTERED_TUPLE = None
|
|
383
|
+
# name -> the globals of the module that registered it. A module imported a
|
|
384
|
+
# second time (importlib.reload, a test that patches sys.modules) runs with
|
|
385
|
+
# new globals under the same __name__ and builds new condition closures, so
|
|
386
|
+
# its rules never compare equal to the first import's.
|
|
387
|
+
_OWNERS = {}
|
|
382
388
|
|
|
383
389
|
|
|
384
390
|
def register(*rules):
|
|
385
391
|
"""Add rules to the ones every handler evaluates. Registering the same
|
|
386
|
-
rule again is a no-op
|
|
392
|
+
rule again is a no-op, and so is a module re-registering its own rules
|
|
393
|
+
when it is imported again (the new rules replace the old). A different
|
|
394
|
+
rule under a taken name is otherwise an error."""
|
|
387
395
|
global _REGISTERED_TUPLE
|
|
396
|
+
caller = sys._getframe(1).f_globals
|
|
388
397
|
for rule in rules:
|
|
389
398
|
if not isinstance(rule, Rule):
|
|
390
399
|
raise ValueError("register() takes rules made by permit() or forbid().")
|
|
391
400
|
known = _REGISTERED.get(rule.name)
|
|
392
|
-
if known is not None and known != rule:
|
|
401
|
+
if known is not None and known != rule and not _reimported(rule.name, caller):
|
|
393
402
|
raise ValueError(f"An authorization rule named '{rule.name}' exists.")
|
|
394
403
|
_REGISTERED[rule.name] = rule
|
|
404
|
+
_OWNERS[rule.name] = caller
|
|
395
405
|
_REGISTERED_TUPLE = None
|
|
396
406
|
|
|
397
407
|
|
|
408
|
+
def _reimported(name, caller):
|
|
409
|
+
"""True when ``caller`` is a fresh import of the module that registered
|
|
410
|
+
``name``: same module name, different globals."""
|
|
411
|
+
owner = _OWNERS.get(name)
|
|
412
|
+
return (
|
|
413
|
+
owner is not None
|
|
414
|
+
and owner is not caller
|
|
415
|
+
and owner.get("__name__") == caller.get("__name__")
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
|
|
398
419
|
def registered():
|
|
399
420
|
"""Every rule a handler evaluates before its own. The same tuple object
|
|
400
421
|
until a rule is registered, so its action index is built once."""
|
|
@@ -528,19 +528,9 @@ class AsyncTransaction:
|
|
|
528
528
|
|
|
529
529
|
# R12 — Slow query logging
|
|
530
530
|
if _SLOW_QUERY_MS and elapsed_ms > _SLOW_QUERY_MS:
|
|
531
|
-
from velocity.db.core.transaction import
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
_logger.warning(
|
|
535
|
-
"Slow async query: %.1f ms sql=%s",
|
|
536
|
-
elapsed_ms,
|
|
537
|
-
sql_preview,
|
|
538
|
-
extra={
|
|
539
|
-
"query_duration_ms": round(elapsed_ms, 1),
|
|
540
|
-
"sql_preview": sql_preview,
|
|
541
|
-
},
|
|
542
|
-
stack_info=True,
|
|
543
|
-
)
|
|
531
|
+
from velocity.db.core.transaction import _slow_query_log
|
|
532
|
+
|
|
533
|
+
_slow_query_log(_logger, sql, elapsed_ms)
|
|
544
534
|
|
|
545
535
|
# R14 — N+1 detection / R33 — raw-write cache invalidation
|
|
546
536
|
from velocity.db.core.transaction import _classify_sql, _extract_table_name
|
velocity/db/core/transaction.py
CHANGED
|
@@ -30,6 +30,15 @@ _DEFAULT_QUERY_CACHE_SIZE = int(os.environ.get("VELOCITY_QUERY_CACHE_SIZE", "100
|
|
|
30
30
|
# Slow-query threshold in milliseconds (0 = disabled).
|
|
31
31
|
_SLOW_QUERY_MS = int(os.environ.get("VELOCITY_SLOW_QUERY_MS", "500"))
|
|
32
32
|
_SLOW_QUERY_SQL_CHARS = int(os.environ.get("VELOCITY_SLOW_QUERY_SQL_CHARS", "4000"))
|
|
33
|
+
# The slow-query line is for people: this much SQL, the caller's file:line,
|
|
34
|
+
# no traceback unless VELOCITY_SLOW_QUERY_STACK=1 asks for one. The full
|
|
35
|
+
# preview (VELOCITY_SLOW_QUERY_SQL_CHARS) still rides on the record's `extra`.
|
|
36
|
+
_SLOW_QUERY_LINE_CHARS = 200
|
|
37
|
+
_SLOW_QUERY_STACK = os.environ.get("VELOCITY_SLOW_QUERY_STACK", "").strip().lower() in (
|
|
38
|
+
"1",
|
|
39
|
+
"true",
|
|
40
|
+
"yes",
|
|
41
|
+
)
|
|
33
42
|
|
|
34
43
|
# N+1 detection: warn when the same table is SELECTed more than this many
|
|
35
44
|
# times within a single transaction. 0 = disabled. Only active when the
|
|
@@ -174,16 +183,79 @@ def _written_tables_from_sql(sql):
|
|
|
174
183
|
return tags or None
|
|
175
184
|
|
|
176
185
|
|
|
186
|
+
# a run of three or more bind placeholders: "IN (%s, %s, %s, …)"
|
|
187
|
+
_PLACEHOLDER_RUN_RE = re.compile(r"(%s|\?)(?:\s*,\s*\1){2,}")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _collapse_placeholders(text):
|
|
191
|
+
"""``%s, %s, … (812 of them)`` -> ``%s × 812``."""
|
|
192
|
+
|
|
193
|
+
def collapse(match):
|
|
194
|
+
token = match.group(1)
|
|
195
|
+
return f"{token} × {match.group(0).count(token)}"
|
|
196
|
+
|
|
197
|
+
return _PLACEHOLDER_RUN_RE.sub(collapse, text)
|
|
198
|
+
|
|
199
|
+
|
|
177
200
|
def _summarize_sql(sql, limit=None):
|
|
178
201
|
if not sql:
|
|
179
202
|
return None
|
|
180
|
-
compact = " ".join(str(sql).split())
|
|
203
|
+
compact = _collapse_placeholders(" ".join(str(sql).split()))
|
|
181
204
|
max_chars = _SLOW_QUERY_SQL_CHARS if limit is None else limit
|
|
182
205
|
if max_chars and len(compact) > max_chars:
|
|
183
206
|
return compact[:max_chars] + "... [truncated]"
|
|
184
207
|
return compact
|
|
185
208
|
|
|
186
209
|
|
|
210
|
+
_VELOCITY_DIR = os.path.dirname(
|
|
211
|
+
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _caller_location():
|
|
216
|
+
"""``file.py:line`` of the first stack frame outside velocity (the code
|
|
217
|
+
that ran the query), or ``None``."""
|
|
218
|
+
import traceback
|
|
219
|
+
|
|
220
|
+
for frame in reversed(traceback.extract_stack()[:-1]):
|
|
221
|
+
filename = os.path.abspath(frame.filename)
|
|
222
|
+
if filename.startswith(_VELOCITY_DIR + os.sep) or "<frozen" in frame.filename:
|
|
223
|
+
continue
|
|
224
|
+
return f"{os.path.basename(frame.filename)}:{frame.lineno}"
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _slow_query_log(logger, sql, elapsed_ms, *, op=None, table=None):
|
|
229
|
+
"""One readable line for a slow query: what, on which table, how long,
|
|
230
|
+
from where, and the start of the SQL (placeholders collapsed). The fields
|
|
231
|
+
for log tooling go in ``extra``; a traceback only with
|
|
232
|
+
VELOCITY_SLOW_QUERY_STACK=1."""
|
|
233
|
+
op = op or _classify_sql(sql)
|
|
234
|
+
table = table if table is not None else _extract_table_name(sql)
|
|
235
|
+
preview = _summarize_sql(sql)
|
|
236
|
+
line = _summarize_sql(sql, limit=_SLOW_QUERY_LINE_CHARS)
|
|
237
|
+
caller = _caller_location()
|
|
238
|
+
took = (
|
|
239
|
+
f"{elapsed_ms / 1000:.1f} s" if elapsed_ms >= 1000 else f"{elapsed_ms:.0f} ms"
|
|
240
|
+
)
|
|
241
|
+
logger.warning(
|
|
242
|
+
"Slow %s%s: %s%s — %s",
|
|
243
|
+
op or "query",
|
|
244
|
+
f" on {table}" if table else "",
|
|
245
|
+
took,
|
|
246
|
+
f" at {caller}" if caller else "",
|
|
247
|
+
line,
|
|
248
|
+
extra={
|
|
249
|
+
"query_duration_ms": round(elapsed_ms, 1),
|
|
250
|
+
"table_name": table,
|
|
251
|
+
"operation": op,
|
|
252
|
+
"sql_preview": preview,
|
|
253
|
+
"caller": caller,
|
|
254
|
+
},
|
|
255
|
+
stack_info=_SLOW_QUERY_STACK,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
187
259
|
def _is_prepare_kw_type_error(error):
|
|
188
260
|
"""True for the TypeError a DB-API driver raises when ``execute()`` has
|
|
189
261
|
no ``prepare`` keyword (``unexpected keyword argument 'prepare'``)."""
|
|
@@ -650,23 +722,7 @@ class Transaction:
|
|
|
650
722
|
|
|
651
723
|
# R12 — Slow query logging.
|
|
652
724
|
if _SLOW_QUERY_MS and elapsed_ms > _SLOW_QUERY_MS:
|
|
653
|
-
|
|
654
|
-
tbl = _extract_table_name(sql)
|
|
655
|
-
sql_preview = _summarize_sql(sql)
|
|
656
|
-
_logger.warning(
|
|
657
|
-
"Slow query (%s): %.1f ms table=%s sql=%s",
|
|
658
|
-
op,
|
|
659
|
-
elapsed_ms,
|
|
660
|
-
tbl,
|
|
661
|
-
sql_preview,
|
|
662
|
-
extra={
|
|
663
|
-
"query_duration_ms": round(elapsed_ms, 1),
|
|
664
|
-
"table_name": tbl,
|
|
665
|
-
"operation": op,
|
|
666
|
-
"sql_preview": sql_preview,
|
|
667
|
-
},
|
|
668
|
-
stack_info=True,
|
|
669
|
-
)
|
|
725
|
+
_slow_query_log(_logger, sql, elapsed_ms)
|
|
670
726
|
|
|
671
727
|
# R14 — N+1 detection (only when debug=True).
|
|
672
728
|
if debug and _N_PLUS_1_THRESHOLD:
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
velocity/__init__.py,sha256=
|
|
1
|
+
velocity/__init__.py,sha256=zI7efmr4RDHUvu7Mw3sLtHToFQqy55c8QIuqnJD8ZRo,362
|
|
2
2
|
velocity/cache.py,sha256=pthvw7gDVEXRzkTcQtA5YVf4Q1IxAamWsba4UReT0RE,8933
|
|
3
3
|
velocity/logging.py,sha256=MAsZap1cDSdOBNRc_9ezQxup5dHOS0ARxU9NI_-UK3E,4037
|
|
4
4
|
velocity/aws/__init__.py,sha256=XJJ79gnPlhg3aJij713PB8-vjQOt-BkqxYeGCBOxAu8,3460
|
|
5
5
|
velocity/aws/amplify.py,sha256=N_763LiraJfuLYSllGKG6jfP2oyCJgtrK_wI2UZKv1E,8852
|
|
6
|
-
velocity/aws/authz.py,sha256=
|
|
6
|
+
velocity/aws/authz.py,sha256=C9D-_W6pKWrHCa8oLj2spFhpHKJ7FAhu_sNHRPl0y-8,50688
|
|
7
7
|
velocity/aws/blobs.py,sha256=2TlTZ3KPxBdXDJ99aVSgDLR11wPQ8O3BbtNbY8mFQ28,9691
|
|
8
8
|
velocity/aws/dirty_pipeline.py,sha256=DQHsCPQPWwiy_E7nxX8kO3DgvQTFkVDaDOjlBcmblyE,3331
|
|
9
9
|
velocity/aws/email.py,sha256=CAvXTAUxJqMaT8ylpRwRcVEA4YLFMUuHCrwYLVE4G1E,6243
|
|
@@ -86,7 +86,7 @@ velocity/db/migrations.py,sha256=ff4kpSrgiBnpdsZ6CHj279_ALf0FE3fy7h2flfUW7A8,222
|
|
|
86
86
|
velocity/db/tunnel.py,sha256=F01GuxRCxtPg7MbG1KYWmQsAVjefzH1jyXDVNCpCJMM,2316
|
|
87
87
|
velocity/db/utils.py,sha256=p-LzHAIexSfMtlr5jGJZR6y0AALCxA347p_tQadEyFw,8973
|
|
88
88
|
velocity/db/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
89
|
-
velocity/db/core/async_support.py,sha256=
|
|
89
|
+
velocity/db/core/async_support.py,sha256=M3Fq-V1sfj9pyeTz8eHXcYp22ThZbISF5-L095T6R8A,26160
|
|
90
90
|
velocity/db/core/column.py,sha256=6RA1Xpk1JY1j4g4yFNWli1C8uZEUyA4NgLmT0oC-VeA,4859
|
|
91
91
|
velocity/db/core/database.py,sha256=6vf0sX8Xt8ynCq7oM09hSFAl_3zsTXv-otOemT0KKGg,3530
|
|
92
92
|
velocity/db/core/decorators.py,sha256=9kkFV3wtjobIbftfiJIZ3q6cNFg7cVE936cmmuWFvCI,19325
|
|
@@ -97,7 +97,7 @@ velocity/db/core/result.py,sha256=Ij0YtKKiUjX2mfG0Kkz8VA0n-X08xyz-vGyYjHoCDwY,15
|
|
|
97
97
|
velocity/db/core/row.py,sha256=A2U5cRW03wFOf15jDr_LUCJe0pNW6RYddG-PK3vYqX4,18777
|
|
98
98
|
velocity/db/core/sequence.py,sha256=dap4Ip0m3htgWV9eCF6ajt-SImnPJJGb8pb8kZVVY4Y,4064
|
|
99
99
|
velocity/db/core/table.py,sha256=DVwCwOaiq-z-dgnazHPwgItx97Sce4SAaFZJGdpklB0,126292
|
|
100
|
-
velocity/db/core/transaction.py,sha256=
|
|
100
|
+
velocity/db/core/transaction.py,sha256=wqmysXnVJvh9vaLlDF4hCux55h21rt7LHzHd6jgEXak,55462
|
|
101
101
|
velocity/db/core/view.py,sha256=lrT1V-NlRioCSfk5pmdXGdCEafdP-t1Pl_EalIrX_cc,5286
|
|
102
102
|
velocity/db/servers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
103
103
|
velocity/db/servers/tablehelper.py,sha256=Is47kUFvFo2kPi7eTe7zj09XM9c7bLNHflr5YlOKBfg,48368
|
|
@@ -223,9 +223,9 @@ velocity/payment/stripe_adapter.py,sha256=6EmVBZZchg2s3Teya0eQGA_8pvSK_teizr036U
|
|
|
223
223
|
velocity/payment/stripe_mirror.py,sha256=6GKJk_PjAgySqFEuxbcuV423OfuGXupVTiGu9QwS5W8,12320
|
|
224
224
|
velocity/testing/__init__.py,sha256=WtYMDHgCLxOq6mlemcOYV5Y9HqF_2QQgSfuAjFRg69Q,731
|
|
225
225
|
velocity/testing/fake_db.py,sha256=phgdulk5owf3noUDpEfI7yFsVcKxRGZ82f2BJPYnaiU,53226
|
|
226
|
-
velocity_python-0.2.
|
|
227
|
-
velocity_python-0.2.
|
|
228
|
-
velocity_python-0.2.
|
|
229
|
-
velocity_python-0.2.
|
|
230
|
-
velocity_python-0.2.
|
|
231
|
-
velocity_python-0.2.
|
|
226
|
+
velocity_python-0.2.6.dist-info/licenses/LICENSE,sha256=aoN245GG8s9oRUU89KNiGTU4_4OtnNmVi4hQeChg6rM,1076
|
|
227
|
+
velocity_python-0.2.6.dist-info/METADATA,sha256=un8yKry0URcUzfnJhcmA7PolBjxeQvmk2WH7Ag0KHo0,17782
|
|
228
|
+
velocity_python-0.2.6.dist-info/WHEEL,sha256=a-udL2m4V_6Yn8b-uKzWb12RjRxwwMo6ohYFM24tpaY,91
|
|
229
|
+
velocity_python-0.2.6.dist-info/entry_points.txt,sha256=nXUKJHkOGQCSPoagdmMNd_qycrle1LVU-GhYknLGM1M,56
|
|
230
|
+
velocity_python-0.2.6.dist-info/top_level.txt,sha256=JW2vJPmodgdgSz7H6yoZvnxF8S3fTMIv-YJWCT1sNW0,9
|
|
231
|
+
velocity_python-0.2.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|