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 CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.2.4"
1
+ __version__ = version = "0.2.6"
2
2
 
3
3
  import importlib as _importlib
4
4
 
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; a different rule under a taken name is an error."""
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 _summarize_sql
532
-
533
- sql_preview = _summarize_sql(sql)
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
@@ -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
- op = _classify_sql(sql)
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: A rapid application development library for interfacing with data storage
5
5
  Author-email: Velocity Team <info@codeclubs.org>
6
6
  License-Expression: MIT
@@ -1,9 +1,9 @@
1
- velocity/__init__.py,sha256=84gbnHEUDdcmEw4DdO9GRo7A5VBhALffvnNP7_LlCW8,362
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=377Cse7T9-TlCbeUvPWtg5j6oMy2tvQi8rCA9892TXc,49811
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=vEZeH8HSr4qkg_W-eGt3gnrUwzcVMb4b87q-GxxIp1g,26490
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=5BMzFGJxA2W9EK45wGB66hGvwcdayKORxJ02tUbPxsE,53597
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.4.dist-info/licenses/LICENSE,sha256=aoN245GG8s9oRUU89KNiGTU4_4OtnNmVi4hQeChg6rM,1076
227
- velocity_python-0.2.4.dist-info/METADATA,sha256=wYu_9tM6EPj1sLFlthIKiPxjPin8VYr3h-TBzoteM5I,17782
228
- velocity_python-0.2.4.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
229
- velocity_python-0.2.4.dist-info/entry_points.txt,sha256=nXUKJHkOGQCSPoagdmMNd_qycrle1LVU-GhYknLGM1M,56
230
- velocity_python-0.2.4.dist-info/top_level.txt,sha256=JW2vJPmodgdgSz7H6yoZvnxF8S3fTMIv-YJWCT1sNW0,9
231
- velocity_python-0.2.4.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (84.0.0)
2
+ Generator: setuptools (84.2.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5