velocity-python 0.2.5__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.5"
1
+ __version__ = version = "0.2.6"
2
2
 
3
3
  import importlib as _importlib
4
4
 
@@ -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.5
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,4 +1,4 @@
1
- velocity/__init__.py,sha256=V1lljCRWKU9tFCI6LxgFTWSNei85X0V-LOuyoqcJS3Y,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
@@ -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.5.dist-info/licenses/LICENSE,sha256=aoN245GG8s9oRUU89KNiGTU4_4OtnNmVi4hQeChg6rM,1076
227
- velocity_python-0.2.5.dist-info/METADATA,sha256=yorIMbxWLu_XdjOPD6muPpfm-qldBUidXT7IR3G01Hc,17782
228
- velocity_python-0.2.5.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
229
- velocity_python-0.2.5.dist-info/entry_points.txt,sha256=nXUKJHkOGQCSPoagdmMNd_qycrle1LVU-GhYknLGM1M,56
230
- velocity_python-0.2.5.dist-info/top_level.txt,sha256=JW2vJPmodgdgSz7H6yoZvnxF8S3fTMIv-YJWCT1sNW0,9
231
- velocity_python-0.2.5.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