sqlobjects 1.6.0__tar.gz → 1.8.0__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.
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/CHANGELOG.md +27 -0
- {sqlobjects-1.6.0/sqlobjects.egg-info → sqlobjects-1.8.0}/PKG-INFO +1 -1
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/pyproject.toml +1 -1
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/__init__.py +4 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/expressions/function.py +45 -8
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/fields/types/comparators.py +8 -2
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/queries/executor.py +30 -0
- sqlobjects-1.8.0/sqlobjects/sql_logging.py +156 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0/sqlobjects.egg-info}/PKG-INFO +1 -1
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects.egg-info/SOURCES.txt +1 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/LICENSE +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/README.md +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/docs/rules/01-database-session-guide.md +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/docs/rules/02-model-definition-guide.md +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/docs/rules/03-query-operations-guide.md +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/docs/rules/04-crud-operations-guide.md +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/docs/rules/05-relationships-guide.md +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/docs/rules/06-validation-signals-guide.md +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/docs/rules/07-performance-guide.md +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/docs/rules/README.md +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/setup.cfg +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/_install_rules.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/cascade.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/contrib/__init__.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/contrib/asgi.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/contrib/fastapi.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/database/__init__.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/database/config.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/database/manager.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/exceptions.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/expressions/__init__.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/expressions/aggregate.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/expressions/base.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/expressions/cte.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/expressions/explain.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/expressions/mixins.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/expressions/scalar.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/expressions/subquery.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/expressions/terminal.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/expressions/window.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/fields/__init__.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/fields/core.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/fields/functions.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/fields/proxies.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/fields/relations/__init__.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/fields/relations/descriptors.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/fields/relations/managers.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/fields/relations/prefetch.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/fields/relations/strategies.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/fields/relations/utils.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/fields/shortcuts.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/fields/types/__init__.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/fields/types/base.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/fields/types/registry.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/fields/utils.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/internal/__init__.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/internal/operations.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/internal/results.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/metadata.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/mixins.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/model.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/objects/__init__.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/objects/bulk.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/objects/core.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/objects/upsert.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/queries/__init__.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/queries/builder.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/queries/dialect.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/queryset.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/session.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/signals.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/utils/__init__.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/utils/inspect.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/utils/naming.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/utils/pattern.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects/validators.py +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects.egg-info/dependency_links.txt +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects.egg-info/entry_points.txt +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects.egg-info/requires.txt +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/sqlobjects.egg-info/top_level.txt +0 -0
- {sqlobjects-1.6.0 → sqlobjects-1.8.0}/tests/test_config.py +0 -0
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
## 1.8.0 (2026-03-26)
|
|
2
|
+
|
|
3
|
+
### Feat
|
|
4
|
+
|
|
5
|
+
- **logging**: export SQLCallerFilter and get_caller_frame in public API
|
|
6
|
+
- **logging**: emit SQL log records in QueryExecutor
|
|
7
|
+
- **logging**: add SQLCallerFilter
|
|
8
|
+
- **logging**: add get_caller_frame() helper
|
|
9
|
+
|
|
10
|
+
### Fix
|
|
11
|
+
|
|
12
|
+
- **logging**: eliminate isEnabledFor race and fix test name
|
|
13
|
+
- **logging**: restore logger level in test_no_log_when_logger_disabled
|
|
14
|
+
- **logging**: guard timing code with isEnabledFor check
|
|
15
|
+
- **logging**: simplify SQLCallerFilter extra_skip_packages handling
|
|
16
|
+
- **logging**: fix frame-skip edge cases and improve code quality
|
|
17
|
+
|
|
18
|
+
## 1.7.0 (2026-03-26)
|
|
19
|
+
|
|
20
|
+
### Feat
|
|
21
|
+
|
|
22
|
+
- **metadata**: add foreignkey() constraint builder
|
|
23
|
+
|
|
24
|
+
### Fix
|
|
25
|
+
|
|
26
|
+
- **raw**: allow SA expressions as arguments in raw() methods
|
|
27
|
+
|
|
1
28
|
## 1.6.0 (2026-03-18)
|
|
2
29
|
|
|
3
30
|
### Feat
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlobjects
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.8.0
|
|
4
4
|
Summary: Django-style async ORM library based on SQLAlchemy with chainable queries, Q objects, and relationship loading
|
|
5
5
|
Author-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
|
|
6
6
|
Maintainer-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
|
|
@@ -16,6 +16,7 @@ from .objects import (
|
|
|
16
16
|
TransactionMode,
|
|
17
17
|
)
|
|
18
18
|
from .queryset import Q, QuerySet
|
|
19
|
+
from .sql_logging import SQLCallerFilter, get_caller_frame
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
__version__ = "0.3.0"
|
|
@@ -41,4 +42,7 @@ __all__ = [
|
|
|
41
42
|
"TransactionMode",
|
|
42
43
|
"ErrorHandling",
|
|
43
44
|
"ConflictResolution",
|
|
45
|
+
# SQL logging
|
|
46
|
+
"SQLCallerFilter",
|
|
47
|
+
"get_caller_frame",
|
|
44
48
|
]
|
|
@@ -183,7 +183,9 @@ class FunctionExpression:
|
|
|
183
183
|
|
|
184
184
|
Args:
|
|
185
185
|
sql: Raw SQL function name or expression
|
|
186
|
-
*args: Arguments to pass to the function. Use ... (Ellipsis) as placeholder for current expression
|
|
186
|
+
*args: Arguments to pass to the function. Use ... (Ellipsis) as placeholder for current expression.
|
|
187
|
+
SQLAlchemy expressions (ColumnElement, FunctionExpression) are passed through as-is;
|
|
188
|
+
plain Python values are wrapped with literal().
|
|
187
189
|
**kwargs: Additional keyword arguments passed to the function
|
|
188
190
|
|
|
189
191
|
Returns:
|
|
@@ -198,12 +200,17 @@ class FunctionExpression:
|
|
|
198
200
|
User.age.avg().raw('CUSTOM_FUNCTION', 'param1', ..., 'param2')
|
|
199
201
|
# Generates: CUSTOM_FUNCTION('param1', avg(age), 'param2')
|
|
200
202
|
|
|
201
|
-
#
|
|
202
|
-
User.
|
|
203
|
-
# Generates: CUSTOM_FUNCTION(
|
|
203
|
+
# Passing another SA expression as argument
|
|
204
|
+
User.name.raw('CUSTOM_FUNCTION', other_func_expr)
|
|
205
|
+
# Generates: CUSTOM_FUNCTION(name, other_func_expr)
|
|
204
206
|
"""
|
|
205
207
|
from sqlalchemy import literal
|
|
206
208
|
|
|
209
|
+
def _to_sql_arg(arg):
|
|
210
|
+
if isinstance(arg, (ColumnElement, FunctionExpression)):
|
|
211
|
+
return arg
|
|
212
|
+
return literal(arg)
|
|
213
|
+
|
|
207
214
|
# Check if ... (Ellipsis) is used as placeholder
|
|
208
215
|
if ... in args:
|
|
209
216
|
# Replace ... with current expression
|
|
@@ -212,12 +219,10 @@ class FunctionExpression:
|
|
|
212
219
|
if arg is ...:
|
|
213
220
|
all_args.append(self.expression)
|
|
214
221
|
else:
|
|
215
|
-
all_args.append(
|
|
222
|
+
all_args.append(_to_sql_arg(arg))
|
|
216
223
|
else:
|
|
217
224
|
# Default behavior: current expression as first argument
|
|
218
|
-
all_args = [self.expression]
|
|
219
|
-
for arg in args:
|
|
220
|
-
all_args.append(literal(arg))
|
|
225
|
+
all_args = [self.expression] + [_to_sql_arg(arg) for arg in args]
|
|
221
226
|
|
|
222
227
|
# Use func to create the raw function call
|
|
223
228
|
raw_func = getattr(_sa_func, sql)
|
|
@@ -425,6 +430,38 @@ class _FuncWrapper:
|
|
|
425
430
|
|
|
426
431
|
def nth_value(self, col: Any, n: int) -> NthValueFunction: ...
|
|
427
432
|
|
|
433
|
+
def raw(self, sql: str, *args) -> FunctionExpression: ...
|
|
434
|
+
|
|
435
|
+
def raw(self, sql: str, *args) -> "FunctionExpression":
|
|
436
|
+
"""Call an arbitrary SQL function by name.
|
|
437
|
+
|
|
438
|
+
Unlike col.raw() / FunctionExpression.raw() which insert the current
|
|
439
|
+
expression as the first argument, this standalone form takes the function
|
|
440
|
+
name and all arguments explicitly.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
sql: SQL function name (e.g. "ts_rank", "to_tsvector")
|
|
444
|
+
*args: Arguments passed to the function. SQLAlchemy expressions
|
|
445
|
+
(ColumnElement, FunctionExpression) are used as-is; plain
|
|
446
|
+
Python values are wrapped with literal().
|
|
447
|
+
|
|
448
|
+
Examples:
|
|
449
|
+
func.raw("ts_rank", DocumentIndexes.content_vector, query_vec)
|
|
450
|
+
# → ts_rank(document_indexes.content_vector, <query_vec>)
|
|
451
|
+
|
|
452
|
+
func.raw("to_tsvector", "'simple'::regconfig", "some text")
|
|
453
|
+
# → to_tsvector('simple'::regconfig, 'some text')
|
|
454
|
+
"""
|
|
455
|
+
from sqlalchemy import literal
|
|
456
|
+
|
|
457
|
+
def _to_sql_arg(arg):
|
|
458
|
+
if isinstance(arg, (ColumnElement, FunctionExpression)):
|
|
459
|
+
return arg
|
|
460
|
+
return literal(arg)
|
|
461
|
+
|
|
462
|
+
sa_func = getattr(_sa_func, sql)
|
|
463
|
+
return FunctionExpression(sa_func(*[_to_sql_arg(a) for a in args]))
|
|
464
|
+
|
|
428
465
|
def __getattr__(self, name: str) -> Any:
|
|
429
466
|
"""Delegate to SQLAlchemy func for all other functions."""
|
|
430
467
|
return getattr(_sa_func, name)
|
|
@@ -50,6 +50,12 @@ class ComparatorMixin:
|
|
|
50
50
|
|
|
51
51
|
def raw(self, sql: str, *args, **kwargs) -> FunctionExpression:
|
|
52
52
|
from sqlalchemy import literal
|
|
53
|
+
from sqlalchemy.sql.elements import ColumnElement
|
|
54
|
+
|
|
55
|
+
def _to_sql_arg(arg):
|
|
56
|
+
if isinstance(arg, (ColumnElement, FunctionExpression)):
|
|
57
|
+
return arg
|
|
58
|
+
return literal(arg)
|
|
53
59
|
|
|
54
60
|
if ... in args:
|
|
55
61
|
all_args = []
|
|
@@ -57,9 +63,9 @@ class ComparatorMixin:
|
|
|
57
63
|
if arg is ...:
|
|
58
64
|
all_args.append(self)
|
|
59
65
|
else:
|
|
60
|
-
all_args.append(
|
|
66
|
+
all_args.append(_to_sql_arg(arg))
|
|
61
67
|
else:
|
|
62
|
-
all_args = [self] + [
|
|
68
|
+
all_args = [self] + [_to_sql_arg(arg) for arg in args]
|
|
63
69
|
raw_func = getattr(func, sql)
|
|
64
70
|
return FunctionExpression(raw_func(*all_args, **kwargs))
|
|
65
71
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import gc
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
3
5
|
from collections.abc import AsyncGenerator
|
|
4
6
|
from typing import Any, TypeVar, overload
|
|
5
7
|
|
|
@@ -13,6 +15,9 @@ from sqlalchemy import (
|
|
|
13
15
|
)
|
|
14
16
|
|
|
15
17
|
|
|
18
|
+
_sql_logger = logging.getLogger("sqlobjects.sql")
|
|
19
|
+
|
|
20
|
+
|
|
16
21
|
_T = TypeVar("_T")
|
|
17
22
|
|
|
18
23
|
|
|
@@ -230,8 +235,33 @@ class QueryExecutor:
|
|
|
230
235
|
else: # exists
|
|
231
236
|
return False
|
|
232
237
|
|
|
238
|
+
# Compile SQL for logging only when the logger is active (avoids overhead)
|
|
239
|
+
_logging_active = _sql_logger.isEnabledFor(logging.DEBUG)
|
|
240
|
+
sql_str = ""
|
|
241
|
+
params: dict = {}
|
|
242
|
+
t0 = 0.0
|
|
243
|
+
if _logging_active:
|
|
244
|
+
try:
|
|
245
|
+
compiled = query.compile(
|
|
246
|
+
dialect=session.bind.dialect,
|
|
247
|
+
compile_kwargs={"literal_binds": False},
|
|
248
|
+
)
|
|
249
|
+
sql_str = str(compiled)
|
|
250
|
+
params = dict(compiled.params) if compiled.params else {}
|
|
251
|
+
except Exception:
|
|
252
|
+
sql_str = str(query)
|
|
253
|
+
params = {}
|
|
254
|
+
t0 = time.perf_counter()
|
|
255
|
+
|
|
233
256
|
result = await session.execute(query)
|
|
234
257
|
|
|
258
|
+
if _logging_active:
|
|
259
|
+
duration_ms = (time.perf_counter() - t0) * 1000
|
|
260
|
+
_sql_logger.debug(
|
|
261
|
+
sql_str,
|
|
262
|
+
extra={"sql": sql_str, "params": params, "duration_ms": duration_ms},
|
|
263
|
+
)
|
|
264
|
+
|
|
235
265
|
if query_type == "all":
|
|
236
266
|
rows = result.fetchall()
|
|
237
267
|
if model_class:
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""SQL logging utilities for sqlobjects.
|
|
2
|
+
|
|
3
|
+
Provides get_caller_frame() to surface user-code caller information in SQL log
|
|
4
|
+
records, compatible with standard logging and loguru.
|
|
5
|
+
|
|
6
|
+
Filter strategy:
|
|
7
|
+
- Skips frames from site-packages (covers pip-installed sqlobjects/sqlalchemy)
|
|
8
|
+
- Skips frames whose module name starts with "sqlobjects." or "sqlalchemy."
|
|
9
|
+
(covers editable installs via `pip install -e .`)
|
|
10
|
+
- Skips frames from extra_skip_packages specified by the caller
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import inspect
|
|
16
|
+
import logging
|
|
17
|
+
import os
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = ["get_caller_frame", "SQLCallerFilter"]
|
|
21
|
+
|
|
22
|
+
# Exact module names that are always considered internal
|
|
23
|
+
_INTERNAL_MODULES = {"sqlobjects", "sqlalchemy"}
|
|
24
|
+
|
|
25
|
+
# Module name prefixes that are always considered internal
|
|
26
|
+
_INTERNAL_PREFIXES = ("sqlobjects.", "sqlalchemy.")
|
|
27
|
+
|
|
28
|
+
# Absolute path of this file, used to skip itself reliably
|
|
29
|
+
_THIS_FILE = os.path.abspath(__file__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_caller_frame(
|
|
33
|
+
extra_skip_packages: list[str] | None = None,
|
|
34
|
+
max_frames: int = 1,
|
|
35
|
+
) -> str | list[str]:
|
|
36
|
+
"""Inspect the call stack and return the first user-code frame(s).
|
|
37
|
+
|
|
38
|
+
Skips frames from:
|
|
39
|
+
- site-packages (pip install)
|
|
40
|
+
- sqlobjects.* and sqlalchemy.* modules (editable install)
|
|
41
|
+
- extra_skip_packages prefixes provided by the caller
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
extra_skip_packages: Additional module name prefixes to skip
|
|
45
|
+
(e.g. ["myapp.middleware"]). Matched against frame's __name__.
|
|
46
|
+
max_frames: How many user-code frames to return.
|
|
47
|
+
1 returns a str; >1 returns a list[str].
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Frame string "path/to/file.py:lineno in funcname", or a list of such
|
|
51
|
+
strings when max_frames > 1.
|
|
52
|
+
"""
|
|
53
|
+
skip_prefixes = _INTERNAL_PREFIXES
|
|
54
|
+
if extra_skip_packages:
|
|
55
|
+
skip_prefixes = skip_prefixes + tuple(extra_skip_packages)
|
|
56
|
+
|
|
57
|
+
frames: list[str] = []
|
|
58
|
+
|
|
59
|
+
for frame_info in inspect.stack():
|
|
60
|
+
filepath = frame_info.filename
|
|
61
|
+
module = frame_info.frame.f_globals.get("__name__", "")
|
|
62
|
+
|
|
63
|
+
# Skip site-packages frames (pip-installed third-party libs)
|
|
64
|
+
if "site-packages" in filepath:
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
# Skip sqlobjects/sqlalchemy frames (editable install)
|
|
68
|
+
if module in _INTERNAL_MODULES or module.startswith(skip_prefixes):
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
# Skip this helper file itself
|
|
72
|
+
if os.path.abspath(filepath) == _THIS_FILE:
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
rel_path = _relative_path(filepath)
|
|
76
|
+
frames.append(f"{rel_path}:{frame_info.lineno} in {frame_info.function}")
|
|
77
|
+
|
|
78
|
+
if len(frames) >= max_frames:
|
|
79
|
+
break
|
|
80
|
+
|
|
81
|
+
if not frames:
|
|
82
|
+
return "<unknown>" if max_frames == 1 else ["<unknown>"]
|
|
83
|
+
|
|
84
|
+
return frames[0] if max_frames == 1 else frames
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _relative_path(filepath: str) -> str:
|
|
88
|
+
"""Return path relative to cwd, or absolute if outside cwd."""
|
|
89
|
+
try:
|
|
90
|
+
return os.path.relpath(filepath)
|
|
91
|
+
except ValueError:
|
|
92
|
+
return filepath
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class SQLCallerFilter(logging.Filter):
|
|
96
|
+
"""logging.Filter that rewrites LogRecord caller fields to user-code location.
|
|
97
|
+
|
|
98
|
+
Inspects the call stack at filter time, skips library frames (site-packages
|
|
99
|
+
and sqlobjects.*/sqlalchemy.* modules for editable installs), and overwrites
|
|
100
|
+
record.filename / record.funcName / record.lineno / record.pathname so that
|
|
101
|
+
any handler (including loguru interception) displays the real user-code
|
|
102
|
+
call site.
|
|
103
|
+
|
|
104
|
+
Also exposes record.caller (str or list[str]) for use in custom Formatters.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
max_frames: Number of user-code frames to capture (default 1).
|
|
108
|
+
When 1, record.caller is a str and record location fields point
|
|
109
|
+
to that single frame.
|
|
110
|
+
When > 1, record.caller is a list[str] and record location fields
|
|
111
|
+
are set from the first (most recent) frame.
|
|
112
|
+
extra_skip_packages: Additional module name prefixes to skip
|
|
113
|
+
(matched against frame's __name__).
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def __init__(
|
|
117
|
+
self,
|
|
118
|
+
max_frames: int = 1,
|
|
119
|
+
extra_skip_packages: list[str] | None = None,
|
|
120
|
+
) -> None:
|
|
121
|
+
super().__init__()
|
|
122
|
+
self.max_frames = max_frames
|
|
123
|
+
self.extra_skip_packages: list[str] | None = list(extra_skip_packages) if extra_skip_packages else None
|
|
124
|
+
|
|
125
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
126
|
+
caller = get_caller_frame(
|
|
127
|
+
extra_skip_packages=self.extra_skip_packages,
|
|
128
|
+
max_frames=self.max_frames,
|
|
129
|
+
)
|
|
130
|
+
record.caller = caller
|
|
131
|
+
|
|
132
|
+
# Overwrite standard location fields from the first user frame
|
|
133
|
+
first = caller if isinstance(caller, str) else caller[0]
|
|
134
|
+
self._overwrite_record_location(record, first)
|
|
135
|
+
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
@staticmethod
|
|
139
|
+
def _overwrite_record_location(record: logging.LogRecord, frame_str: str) -> None:
|
|
140
|
+
"""Parse 'path/to/file.py:lineno in funcname' and overwrite record fields.
|
|
141
|
+
|
|
142
|
+
Frame string format: "relative/path/to/file.py:42 in func_name"
|
|
143
|
+
Uses rsplit to handle edge cases where function name could have spaces.
|
|
144
|
+
"""
|
|
145
|
+
try:
|
|
146
|
+
# Split off the function name part (rightmost " in ")
|
|
147
|
+
path_part, func_part = frame_str.rsplit(" in ", 1)
|
|
148
|
+
# Split off the line number (rightmost ":")
|
|
149
|
+
filepath, lineno_str = path_part.rsplit(":", 1)
|
|
150
|
+
record.pathname = os.path.abspath(filepath)
|
|
151
|
+
record.filename = os.path.basename(filepath)
|
|
152
|
+
record.module = os.path.splitext(record.filename)[0]
|
|
153
|
+
record.funcName = func_part.strip()
|
|
154
|
+
record.lineno = int(lineno_str)
|
|
155
|
+
except (ValueError, AttributeError):
|
|
156
|
+
pass # Keep original fields if parsing fails
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlobjects
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.8.0
|
|
4
4
|
Summary: Django-style async ORM library based on SQLAlchemy with chainable queries, Q objects, and relationship loading
|
|
5
5
|
Author-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
|
|
6
6
|
Maintainer-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|