sqlobjects 1.7.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.
Files changed (81) hide show
  1. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/CHANGELOG.md +17 -0
  2. {sqlobjects-1.7.0/sqlobjects.egg-info → sqlobjects-1.8.0}/PKG-INFO +1 -1
  3. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/pyproject.toml +1 -1
  4. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/__init__.py +4 -0
  5. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/queries/executor.py +30 -0
  6. sqlobjects-1.8.0/sqlobjects/sql_logging.py +156 -0
  7. {sqlobjects-1.7.0 → sqlobjects-1.8.0/sqlobjects.egg-info}/PKG-INFO +1 -1
  8. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects.egg-info/SOURCES.txt +1 -0
  9. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/LICENSE +0 -0
  10. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/README.md +0 -0
  11. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/docs/rules/01-database-session-guide.md +0 -0
  12. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/docs/rules/02-model-definition-guide.md +0 -0
  13. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/docs/rules/03-query-operations-guide.md +0 -0
  14. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/docs/rules/04-crud-operations-guide.md +0 -0
  15. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/docs/rules/05-relationships-guide.md +0 -0
  16. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/docs/rules/06-validation-signals-guide.md +0 -0
  17. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/docs/rules/07-performance-guide.md +0 -0
  18. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/docs/rules/README.md +0 -0
  19. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/setup.cfg +0 -0
  20. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/_install_rules.py +0 -0
  21. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/cascade.py +0 -0
  22. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/contrib/__init__.py +0 -0
  23. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/contrib/asgi.py +0 -0
  24. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/contrib/fastapi.py +0 -0
  25. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/database/__init__.py +0 -0
  26. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/database/config.py +0 -0
  27. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/database/manager.py +0 -0
  28. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/exceptions.py +0 -0
  29. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/expressions/__init__.py +0 -0
  30. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/expressions/aggregate.py +0 -0
  31. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/expressions/base.py +0 -0
  32. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/expressions/cte.py +0 -0
  33. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/expressions/explain.py +0 -0
  34. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/expressions/function.py +0 -0
  35. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/expressions/mixins.py +0 -0
  36. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/expressions/scalar.py +0 -0
  37. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/expressions/subquery.py +0 -0
  38. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/expressions/terminal.py +0 -0
  39. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/expressions/window.py +0 -0
  40. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/fields/__init__.py +0 -0
  41. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/fields/core.py +0 -0
  42. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/fields/functions.py +0 -0
  43. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/fields/proxies.py +0 -0
  44. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/fields/relations/__init__.py +0 -0
  45. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/fields/relations/descriptors.py +0 -0
  46. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/fields/relations/managers.py +0 -0
  47. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/fields/relations/prefetch.py +0 -0
  48. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/fields/relations/strategies.py +0 -0
  49. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/fields/relations/utils.py +0 -0
  50. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/fields/shortcuts.py +0 -0
  51. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/fields/types/__init__.py +0 -0
  52. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/fields/types/base.py +0 -0
  53. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/fields/types/comparators.py +0 -0
  54. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/fields/types/registry.py +0 -0
  55. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/fields/utils.py +0 -0
  56. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/internal/__init__.py +0 -0
  57. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/internal/operations.py +0 -0
  58. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/internal/results.py +0 -0
  59. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/metadata.py +0 -0
  60. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/mixins.py +0 -0
  61. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/model.py +0 -0
  62. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/objects/__init__.py +0 -0
  63. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/objects/bulk.py +0 -0
  64. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/objects/core.py +0 -0
  65. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/objects/upsert.py +0 -0
  66. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/queries/__init__.py +0 -0
  67. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/queries/builder.py +0 -0
  68. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/queries/dialect.py +0 -0
  69. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/queryset.py +0 -0
  70. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/session.py +0 -0
  71. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/signals.py +0 -0
  72. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/utils/__init__.py +0 -0
  73. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/utils/inspect.py +0 -0
  74. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/utils/naming.py +0 -0
  75. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/utils/pattern.py +0 -0
  76. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects/validators.py +0 -0
  77. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects.egg-info/dependency_links.txt +0 -0
  78. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects.egg-info/entry_points.txt +0 -0
  79. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects.egg-info/requires.txt +0 -0
  80. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/sqlobjects.egg-info/top_level.txt +0 -0
  81. {sqlobjects-1.7.0 → sqlobjects-1.8.0}/tests/test_config.py +0 -0
@@ -1,3 +1,20 @@
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
+
1
18
  ## 1.7.0 (2026-03-26)
2
19
 
3
20
  ### Feat
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlobjects
3
- Version: 1.7.0
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>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlobjects"
3
- version = "1.7.0"
3
+ version = "1.8.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 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
  ]
@@ -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.7.0
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>
@@ -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