sqlobjects 1.7.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.
Files changed (81) hide show
  1. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/CHANGELOG.md +35 -0
  2. {sqlobjects-1.7.0/sqlobjects.egg-info → sqlobjects-1.9.0}/PKG-INFO +1 -1
  3. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/pyproject.toml +1 -1
  4. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/__init__.py +4 -0
  5. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/queries/executor.py +29 -0
  6. sqlobjects-1.9.0/sqlobjects/sql_logging.py +200 -0
  7. {sqlobjects-1.7.0 → sqlobjects-1.9.0/sqlobjects.egg-info}/PKG-INFO +1 -1
  8. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects.egg-info/SOURCES.txt +1 -0
  9. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/LICENSE +0 -0
  10. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/README.md +0 -0
  11. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/docs/rules/01-database-session-guide.md +0 -0
  12. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/docs/rules/02-model-definition-guide.md +0 -0
  13. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/docs/rules/03-query-operations-guide.md +0 -0
  14. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/docs/rules/04-crud-operations-guide.md +0 -0
  15. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/docs/rules/05-relationships-guide.md +0 -0
  16. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/docs/rules/06-validation-signals-guide.md +0 -0
  17. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/docs/rules/07-performance-guide.md +0 -0
  18. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/docs/rules/README.md +0 -0
  19. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/setup.cfg +0 -0
  20. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/_install_rules.py +0 -0
  21. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/cascade.py +0 -0
  22. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/contrib/__init__.py +0 -0
  23. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/contrib/asgi.py +0 -0
  24. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/contrib/fastapi.py +0 -0
  25. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/database/__init__.py +0 -0
  26. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/database/config.py +0 -0
  27. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/database/manager.py +0 -0
  28. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/exceptions.py +0 -0
  29. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/expressions/__init__.py +0 -0
  30. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/expressions/aggregate.py +0 -0
  31. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/expressions/base.py +0 -0
  32. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/expressions/cte.py +0 -0
  33. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/expressions/explain.py +0 -0
  34. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/expressions/function.py +0 -0
  35. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/expressions/mixins.py +0 -0
  36. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/expressions/scalar.py +0 -0
  37. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/expressions/subquery.py +0 -0
  38. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/expressions/terminal.py +0 -0
  39. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/expressions/window.py +0 -0
  40. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/fields/__init__.py +0 -0
  41. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/fields/core.py +0 -0
  42. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/fields/functions.py +0 -0
  43. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/fields/proxies.py +0 -0
  44. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/fields/relations/__init__.py +0 -0
  45. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/fields/relations/descriptors.py +0 -0
  46. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/fields/relations/managers.py +0 -0
  47. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/fields/relations/prefetch.py +0 -0
  48. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/fields/relations/strategies.py +0 -0
  49. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/fields/relations/utils.py +0 -0
  50. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/fields/shortcuts.py +0 -0
  51. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/fields/types/__init__.py +0 -0
  52. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/fields/types/base.py +0 -0
  53. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/fields/types/comparators.py +0 -0
  54. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/fields/types/registry.py +0 -0
  55. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/fields/utils.py +0 -0
  56. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/internal/__init__.py +0 -0
  57. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/internal/operations.py +0 -0
  58. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/internal/results.py +0 -0
  59. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/metadata.py +0 -0
  60. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/mixins.py +0 -0
  61. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/model.py +0 -0
  62. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/objects/__init__.py +0 -0
  63. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/objects/bulk.py +0 -0
  64. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/objects/core.py +0 -0
  65. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/objects/upsert.py +0 -0
  66. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/queries/__init__.py +0 -0
  67. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/queries/builder.py +0 -0
  68. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/queries/dialect.py +0 -0
  69. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/queryset.py +0 -0
  70. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/session.py +0 -0
  71. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/signals.py +0 -0
  72. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/utils/__init__.py +0 -0
  73. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/utils/inspect.py +0 -0
  74. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/utils/naming.py +0 -0
  75. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/utils/pattern.py +0 -0
  76. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects/validators.py +0 -0
  77. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects.egg-info/dependency_links.txt +0 -0
  78. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects.egg-info/entry_points.txt +0 -0
  79. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects.egg-info/requires.txt +0 -0
  80. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/sqlobjects.egg-info/top_level.txt +0 -0
  81. {sqlobjects-1.7.0 → sqlobjects-1.9.0}/tests/test_config.py +0 -0
@@ -1,3 +1,38 @@
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
+
19
+ ## 1.8.0 (2026-03-26)
20
+
21
+ ### Feat
22
+
23
+ - **logging**: export SQLCallerFilter and get_caller_frame in public API
24
+ - **logging**: emit SQL log records in QueryExecutor
25
+ - **logging**: add SQLCallerFilter
26
+ - **logging**: add get_caller_frame() helper
27
+
28
+ ### Fix
29
+
30
+ - **logging**: eliminate isEnabledFor race and fix test name
31
+ - **logging**: restore logger level in test_no_log_when_logger_disabled
32
+ - **logging**: guard timing code with isEnabledFor check
33
+ - **logging**: simplify SQLCallerFilter extra_skip_packages handling
34
+ - **logging**: fix frame-skip edge cases and improve code quality
35
+
1
36
  ## 1.7.0 (2026-03-26)
2
37
 
3
38
  ### Feat
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlobjects
3
- Version: 1.7.0
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlobjects"
3
- version = "1.7.0"
3
+ version = "1.9.0"
4
4
  description = "Django-style async ORM library based on SQLAlchemy with chainable queries, Q objects, and relationship loading"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -16,6 +16,7 @@ from .objects import (
16
16
  TransactionMode,
17
17
  )
18
18
  from .queryset import Q, QuerySet
19
+ from .sql_logging import ObjectLogger, 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
+ "ObjectLogger",
47
+ "get_caller_frame",
44
48
  ]
@@ -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
 
@@ -12,6 +14,8 @@ from sqlalchemy import (
12
14
  update,
13
15
  )
14
16
 
17
+ from ..sql_logging import _sql_logger
18
+
15
19
 
16
20
  _T = TypeVar("_T")
17
21
 
@@ -230,8 +234,33 @@ class QueryExecutor:
230
234
  else: # exists
231
235
  return False
232
236
 
237
+ # Compile SQL for logging only when the logger is active (avoids overhead)
238
+ _logging_active = _sql_logger.isEnabledFor(logging.DEBUG)
239
+ sql_str = ""
240
+ params: dict = {}
241
+ t0 = 0.0
242
+ if _logging_active:
243
+ try:
244
+ compiled = query.compile(
245
+ dialect=session.bind.dialect,
246
+ compile_kwargs={"literal_binds": False},
247
+ )
248
+ sql_str = str(compiled)
249
+ params = dict(compiled.params) if compiled.params else {}
250
+ except Exception:
251
+ sql_str = str(query)
252
+ params = {}
253
+ t0 = time.perf_counter()
254
+
233
255
  result = await session.execute(query)
234
256
 
257
+ if _logging_active:
258
+ duration_ms = (time.perf_counter() - t0) * 1000
259
+ _sql_logger.debug(
260
+ sql_str,
261
+ extra={"sql": sql_str, "params": params, "duration_ms": duration_ms},
262
+ )
263
+
235
264
  if query_type == "all":
236
265
  rows = result.fetchall()
237
266
  if model_class:
@@ -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.7.0
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>
@@ -20,6 +20,7 @@ sqlobjects/model.py
20
20
  sqlobjects/queryset.py
21
21
  sqlobjects/session.py
22
22
  sqlobjects/signals.py
23
+ sqlobjects/sql_logging.py
23
24
  sqlobjects/validators.py
24
25
  sqlobjects.egg-info/PKG-INFO
25
26
  sqlobjects.egg-info/SOURCES.txt
File without changes
File without changes
File without changes