sqlobjects 1.8.0__tar.gz → 1.9.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.8.0 → sqlobjects-1.9.0}/CHANGELOG.md +18 -0
- {sqlobjects-1.8.0/sqlobjects.egg-info → sqlobjects-1.9.0}/PKG-INFO +1 -1
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/pyproject.toml +1 -1
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/__init__.py +2 -2
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/queries/executor.py +1 -2
- sqlobjects-1.9.0/sqlobjects/sql_logging.py +200 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0/sqlobjects.egg-info}/PKG-INFO +1 -1
- sqlobjects-1.8.0/sqlobjects/sql_logging.py +0 -156
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/LICENSE +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/README.md +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/docs/rules/01-database-session-guide.md +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/docs/rules/02-model-definition-guide.md +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/docs/rules/03-query-operations-guide.md +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/docs/rules/04-crud-operations-guide.md +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/docs/rules/05-relationships-guide.md +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/docs/rules/06-validation-signals-guide.md +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/docs/rules/07-performance-guide.md +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/docs/rules/README.md +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/setup.cfg +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/_install_rules.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/cascade.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/contrib/__init__.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/contrib/asgi.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/contrib/fastapi.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/database/__init__.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/database/config.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/database/manager.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/exceptions.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/expressions/__init__.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/expressions/aggregate.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/expressions/base.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/expressions/cte.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/expressions/explain.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/expressions/function.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/expressions/mixins.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/expressions/scalar.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/expressions/subquery.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/expressions/terminal.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/expressions/window.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/fields/__init__.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/fields/core.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/fields/functions.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/fields/proxies.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/fields/relations/__init__.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/fields/relations/descriptors.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/fields/relations/managers.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/fields/relations/prefetch.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/fields/relations/strategies.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/fields/relations/utils.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/fields/shortcuts.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/fields/types/__init__.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/fields/types/base.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/fields/types/comparators.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/fields/types/registry.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/fields/utils.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/internal/__init__.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/internal/operations.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/internal/results.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/metadata.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/mixins.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/model.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/objects/__init__.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/objects/bulk.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/objects/core.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/objects/upsert.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/queries/__init__.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/queries/builder.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/queries/dialect.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/queryset.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/session.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/signals.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/utils/__init__.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/utils/inspect.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/utils/naming.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/utils/pattern.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/validators.py +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects.egg-info/SOURCES.txt +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects.egg-info/dependency_links.txt +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects.egg-info/entry_points.txt +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects.egg-info/requires.txt +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects.egg-info/top_level.txt +0 -0
- {sqlobjects-1.8.0 → sqlobjects-1.9.0}/tests/test_config.py +0 -0
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
## 1.9.0 (2026-03-27)
|
|
2
|
+
|
|
3
|
+
### Feat
|
|
4
|
+
|
|
5
|
+
- **logging**: wire ObjectLogger into QueryExecutor, zero-config caller rewriting
|
|
6
|
+
- **logging**: replace SQLCallerFilter with ObjectLogger, remove SQLCallerFilter
|
|
7
|
+
- **logging**: add ObjectLogger and _install_object_logger
|
|
8
|
+
|
|
9
|
+
### Fix
|
|
10
|
+
|
|
11
|
+
- **logging**: skip stdlib logging frames in _should_skip_frame, add end-to-end test
|
|
12
|
+
- **logging**: use logging._lock context manager for Python 3.13 compatibility
|
|
13
|
+
- **logging**: add _should_skip_frame tests and fix assertion quality
|
|
14
|
+
|
|
15
|
+
### Refactor
|
|
16
|
+
|
|
17
|
+
- **logging**: extract _should_skip_frame and add _find_user_frame
|
|
18
|
+
|
|
1
19
|
## 1.8.0 (2026-03-26)
|
|
2
20
|
|
|
3
21
|
### Feat
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlobjects
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.9.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,7 +16,7 @@ from .objects import (
|
|
|
16
16
|
TransactionMode,
|
|
17
17
|
)
|
|
18
18
|
from .queryset import Q, QuerySet
|
|
19
|
-
from .sql_logging import
|
|
19
|
+
from .sql_logging import ObjectLogger, get_caller_frame
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
__version__ = "0.3.0"
|
|
@@ -43,6 +43,6 @@ __all__ = [
|
|
|
43
43
|
"ErrorHandling",
|
|
44
44
|
"ConflictResolution",
|
|
45
45
|
# SQL logging
|
|
46
|
-
"
|
|
46
|
+
"ObjectLogger",
|
|
47
47
|
"get_caller_frame",
|
|
48
48
|
]
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""SQL logging utilities for sqlobjects.
|
|
2
|
+
|
|
3
|
+
Provides ObjectLogger (a logging.Logger subclass that auto-rewrites caller
|
|
4
|
+
fields to user-code location) and get_caller_frame() for custom scenarios.
|
|
5
|
+
|
|
6
|
+
Frame-skip 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
|
+
from collections.abc import Mapping
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
__all__ = ["get_caller_frame", "ObjectLogger"]
|
|
23
|
+
|
|
24
|
+
# Exact module names that are always considered internal
|
|
25
|
+
_INTERNAL_MODULES = {"sqlobjects", "sqlalchemy", "logging"}
|
|
26
|
+
|
|
27
|
+
# Module name prefixes that are always considered internal
|
|
28
|
+
_INTERNAL_PREFIXES = ("sqlobjects.", "sqlalchemy.")
|
|
29
|
+
|
|
30
|
+
# Absolute path of this file, used to skip itself reliably
|
|
31
|
+
_THIS_FILE = os.path.abspath(__file__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _should_skip_frame(
|
|
35
|
+
filepath: str,
|
|
36
|
+
module: str,
|
|
37
|
+
extra_skip_prefixes: tuple[str, ...],
|
|
38
|
+
) -> bool:
|
|
39
|
+
"""Return True if this frame should be skipped (library/internal frame).
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
filepath: frame_info.filename
|
|
43
|
+
module: frame.f_globals.get("__name__", "")
|
|
44
|
+
extra_skip_prefixes: tuple of module name prefixes to skip in addition
|
|
45
|
+
to the built-in sqlobjects/sqlalchemy prefixes.
|
|
46
|
+
"""
|
|
47
|
+
if "site-packages" in filepath:
|
|
48
|
+
return True
|
|
49
|
+
if filepath.startswith("<"):
|
|
50
|
+
return True
|
|
51
|
+
if module in _INTERNAL_MODULES or module.startswith(_INTERNAL_PREFIXES):
|
|
52
|
+
return True
|
|
53
|
+
if filepath == _THIS_FILE or os.path.abspath(filepath) == _THIS_FILE:
|
|
54
|
+
return True
|
|
55
|
+
if extra_skip_prefixes and module.startswith(extra_skip_prefixes):
|
|
56
|
+
return True
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _find_user_frame(
|
|
61
|
+
extra_skip_packages: list[str] | None = None,
|
|
62
|
+
) -> inspect.FrameInfo | None:
|
|
63
|
+
"""Return the first user-code FrameInfo, skipping library frames.
|
|
64
|
+
|
|
65
|
+
Applies the same skip rules as get_caller_frame() but returns the raw
|
|
66
|
+
FrameInfo instead of a formatted string. Returns None if no frame found.
|
|
67
|
+
"""
|
|
68
|
+
extra_skip_prefixes: tuple[str, ...] = tuple(extra_skip_packages) if extra_skip_packages else ()
|
|
69
|
+
for frame_info in inspect.stack():
|
|
70
|
+
module = frame_info.frame.f_globals.get("__name__", "")
|
|
71
|
+
if not _should_skip_frame(frame_info.filename, module, extra_skip_prefixes):
|
|
72
|
+
return frame_info
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_caller_frame(
|
|
77
|
+
extra_skip_packages: list[str] | None = None,
|
|
78
|
+
max_frames: int = 1,
|
|
79
|
+
) -> str | list[str]:
|
|
80
|
+
"""Inspect the call stack and return the first user-code frame(s).
|
|
81
|
+
|
|
82
|
+
Skips frames from:
|
|
83
|
+
- site-packages (pip install)
|
|
84
|
+
- sqlobjects.* and sqlalchemy.* modules (editable install)
|
|
85
|
+
- extra_skip_packages prefixes provided by the caller
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
extra_skip_packages: Additional module name prefixes to skip
|
|
89
|
+
(e.g. ["myapp.middleware"]). Matched against frame's __name__.
|
|
90
|
+
max_frames: How many user-code frames to return.
|
|
91
|
+
1 returns a str; >1 returns a list[str].
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Frame string "path/to/file.py:lineno in funcname", or a list of such
|
|
95
|
+
strings when max_frames > 1.
|
|
96
|
+
"""
|
|
97
|
+
extra_skip_prefixes: tuple[str, ...] = tuple(extra_skip_packages) if extra_skip_packages else ()
|
|
98
|
+
frames: list[str] = []
|
|
99
|
+
|
|
100
|
+
for frame_info in inspect.stack():
|
|
101
|
+
module = frame_info.frame.f_globals.get("__name__", "")
|
|
102
|
+
if _should_skip_frame(frame_info.filename, module, extra_skip_prefixes):
|
|
103
|
+
continue
|
|
104
|
+
rel_path = _relative_path(frame_info.filename)
|
|
105
|
+
frames.append(f"{rel_path}:{frame_info.lineno} in {frame_info.function}")
|
|
106
|
+
if len(frames) >= max_frames:
|
|
107
|
+
break
|
|
108
|
+
|
|
109
|
+
if not frames:
|
|
110
|
+
return "<unknown>" if max_frames == 1 else ["<unknown>"]
|
|
111
|
+
return frames[0] if max_frames == 1 else frames
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _relative_path(filepath: str) -> str:
|
|
115
|
+
"""Return path relative to cwd, or absolute if outside cwd."""
|
|
116
|
+
try:
|
|
117
|
+
return os.path.relpath(filepath)
|
|
118
|
+
except ValueError:
|
|
119
|
+
return filepath
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class ObjectLogger(logging.Logger):
|
|
123
|
+
"""logging.Logger subclass that rewrites LogRecord caller fields to user-code location.
|
|
124
|
+
|
|
125
|
+
Overrides makeRecord() to replace the standard caller fields
|
|
126
|
+
(filename, funcName, lineno, pathname) with the first user-code frame
|
|
127
|
+
found by _find_user_frame(). This means any handler attached to this logger
|
|
128
|
+
— including loguru InterceptHandlers — will display the real call site
|
|
129
|
+
without any additional Filter configuration.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
name: Logger name (passed to logging.Logger).
|
|
133
|
+
level: Initial log level (default NOTSET).
|
|
134
|
+
extra_skip_packages: Additional module name prefixes to skip when
|
|
135
|
+
searching for the user-code frame (e.g. ["myapp.middleware"]).
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
def __init__(
|
|
139
|
+
self,
|
|
140
|
+
name: str,
|
|
141
|
+
level: int = logging.NOTSET,
|
|
142
|
+
extra_skip_packages: list[str] | None = None,
|
|
143
|
+
) -> None:
|
|
144
|
+
super().__init__(name, level)
|
|
145
|
+
self.extra_skip_packages = extra_skip_packages
|
|
146
|
+
|
|
147
|
+
def makeRecord( # noqa: N802
|
|
148
|
+
self,
|
|
149
|
+
name: str,
|
|
150
|
+
level: int,
|
|
151
|
+
fn: str,
|
|
152
|
+
lno: int,
|
|
153
|
+
msg: object,
|
|
154
|
+
args: Any,
|
|
155
|
+
exc_info: Any,
|
|
156
|
+
func: str | None = None,
|
|
157
|
+
extra: Mapping[str, object] | None = None,
|
|
158
|
+
sinfo: str | None = None,
|
|
159
|
+
) -> logging.LogRecord:
|
|
160
|
+
record = super().makeRecord(name, level, fn, lno, msg, args, exc_info, func, extra, sinfo)
|
|
161
|
+
frame_info = _find_user_frame(self.extra_skip_packages)
|
|
162
|
+
if frame_info:
|
|
163
|
+
record.pathname = os.path.abspath(frame_info.filename)
|
|
164
|
+
record.filename = os.path.basename(frame_info.filename)
|
|
165
|
+
record.module = os.path.splitext(record.filename)[0]
|
|
166
|
+
record.funcName = frame_info.function
|
|
167
|
+
record.lineno = frame_info.lineno
|
|
168
|
+
return record
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _install_object_logger(name: str) -> ObjectLogger:
|
|
172
|
+
"""Create an ObjectLogger and register it in the logging system.
|
|
173
|
+
|
|
174
|
+
Directly writes into logging.root.manager.loggerDict so that
|
|
175
|
+
logging.getLogger(name) returns this ObjectLogger instance.
|
|
176
|
+
Migrates handlers, level, and propagate from any pre-existing Logger
|
|
177
|
+
(but not from PlaceHolder sentinels, which are logging internals).
|
|
178
|
+
|
|
179
|
+
Thread-safe: acquires the logging module lock during the operation.
|
|
180
|
+
|
|
181
|
+
Known limitation: code that obtained a reference via getLogger(name)
|
|
182
|
+
*before* this function runs will still hold the old Logger instance.
|
|
183
|
+
"""
|
|
184
|
+
with logging._lock: # type: ignore[attr-defined] # noqa: SLF001
|
|
185
|
+
existing = logging.root.manager.loggerDict.get(name)
|
|
186
|
+
logger = ObjectLogger(name)
|
|
187
|
+
logger.parent = logging.root
|
|
188
|
+
logger.propagate = True
|
|
189
|
+
if isinstance(existing, logging.Logger):
|
|
190
|
+
for handler in existing.handlers:
|
|
191
|
+
logger.addHandler(handler)
|
|
192
|
+
if existing.level != logging.NOTSET:
|
|
193
|
+
logger.setLevel(existing.level)
|
|
194
|
+
logger.propagate = existing.propagate
|
|
195
|
+
logging.root.manager.loggerDict[name] = logger
|
|
196
|
+
return logger
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# Module-level instance — imported by executor.py and usable by callers
|
|
200
|
+
_sql_logger: ObjectLogger = _install_object_logger("sqlobjects.sql")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlobjects
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.9.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>
|
|
@@ -1,156 +0,0 @@
|
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|