sqlspec 0.14.0__py3-none-any.whl → 0.15.0__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.

Potentially problematic release.


This version of sqlspec might be problematic. Click here for more details.

Files changed (158) hide show
  1. sqlspec/__init__.py +50 -25
  2. sqlspec/__main__.py +12 -0
  3. sqlspec/__metadata__.py +1 -3
  4. sqlspec/_serialization.py +1 -2
  5. sqlspec/_sql.py +256 -120
  6. sqlspec/_typing.py +278 -142
  7. sqlspec/adapters/adbc/__init__.py +4 -3
  8. sqlspec/adapters/adbc/_types.py +12 -0
  9. sqlspec/adapters/adbc/config.py +115 -248
  10. sqlspec/adapters/adbc/driver.py +462 -353
  11. sqlspec/adapters/aiosqlite/__init__.py +18 -3
  12. sqlspec/adapters/aiosqlite/_types.py +13 -0
  13. sqlspec/adapters/aiosqlite/config.py +199 -129
  14. sqlspec/adapters/aiosqlite/driver.py +230 -269
  15. sqlspec/adapters/asyncmy/__init__.py +18 -3
  16. sqlspec/adapters/asyncmy/_types.py +12 -0
  17. sqlspec/adapters/asyncmy/config.py +80 -168
  18. sqlspec/adapters/asyncmy/driver.py +260 -225
  19. sqlspec/adapters/asyncpg/__init__.py +19 -4
  20. sqlspec/adapters/asyncpg/_types.py +17 -0
  21. sqlspec/adapters/asyncpg/config.py +82 -181
  22. sqlspec/adapters/asyncpg/driver.py +285 -383
  23. sqlspec/adapters/bigquery/__init__.py +17 -3
  24. sqlspec/adapters/bigquery/_types.py +12 -0
  25. sqlspec/adapters/bigquery/config.py +191 -258
  26. sqlspec/adapters/bigquery/driver.py +474 -646
  27. sqlspec/adapters/duckdb/__init__.py +14 -3
  28. sqlspec/adapters/duckdb/_types.py +12 -0
  29. sqlspec/adapters/duckdb/config.py +415 -351
  30. sqlspec/adapters/duckdb/driver.py +343 -413
  31. sqlspec/adapters/oracledb/__init__.py +19 -5
  32. sqlspec/adapters/oracledb/_types.py +14 -0
  33. sqlspec/adapters/oracledb/config.py +123 -379
  34. sqlspec/adapters/oracledb/driver.py +507 -560
  35. sqlspec/adapters/psqlpy/__init__.py +13 -3
  36. sqlspec/adapters/psqlpy/_types.py +11 -0
  37. sqlspec/adapters/psqlpy/config.py +93 -254
  38. sqlspec/adapters/psqlpy/driver.py +505 -234
  39. sqlspec/adapters/psycopg/__init__.py +19 -5
  40. sqlspec/adapters/psycopg/_types.py +17 -0
  41. sqlspec/adapters/psycopg/config.py +143 -403
  42. sqlspec/adapters/psycopg/driver.py +706 -872
  43. sqlspec/adapters/sqlite/__init__.py +14 -3
  44. sqlspec/adapters/sqlite/_types.py +11 -0
  45. sqlspec/adapters/sqlite/config.py +202 -118
  46. sqlspec/adapters/sqlite/driver.py +264 -303
  47. sqlspec/base.py +105 -9
  48. sqlspec/{statement/builder → builder}/__init__.py +12 -14
  49. sqlspec/{statement/builder → builder}/_base.py +120 -55
  50. sqlspec/{statement/builder → builder}/_column.py +17 -6
  51. sqlspec/{statement/builder → builder}/_ddl.py +46 -79
  52. sqlspec/{statement/builder → builder}/_ddl_utils.py +5 -10
  53. sqlspec/{statement/builder → builder}/_delete.py +6 -25
  54. sqlspec/{statement/builder → builder}/_insert.py +6 -64
  55. sqlspec/builder/_merge.py +56 -0
  56. sqlspec/{statement/builder → builder}/_parsing_utils.py +3 -10
  57. sqlspec/{statement/builder → builder}/_select.py +11 -56
  58. sqlspec/{statement/builder → builder}/_update.py +12 -18
  59. sqlspec/{statement/builder → builder}/mixins/__init__.py +10 -14
  60. sqlspec/{statement/builder → builder}/mixins/_cte_and_set_ops.py +48 -59
  61. sqlspec/{statement/builder → builder}/mixins/_insert_operations.py +22 -16
  62. sqlspec/{statement/builder → builder}/mixins/_join_operations.py +1 -3
  63. sqlspec/{statement/builder → builder}/mixins/_merge_operations.py +3 -5
  64. sqlspec/{statement/builder → builder}/mixins/_order_limit_operations.py +3 -3
  65. sqlspec/{statement/builder → builder}/mixins/_pivot_operations.py +4 -8
  66. sqlspec/{statement/builder → builder}/mixins/_select_operations.py +21 -36
  67. sqlspec/{statement/builder → builder}/mixins/_update_operations.py +3 -14
  68. sqlspec/{statement/builder → builder}/mixins/_where_clause.py +52 -79
  69. sqlspec/cli.py +4 -5
  70. sqlspec/config.py +180 -133
  71. sqlspec/core/__init__.py +63 -0
  72. sqlspec/core/cache.py +873 -0
  73. sqlspec/core/compiler.py +396 -0
  74. sqlspec/core/filters.py +828 -0
  75. sqlspec/core/hashing.py +310 -0
  76. sqlspec/core/parameters.py +1209 -0
  77. sqlspec/core/result.py +664 -0
  78. sqlspec/{statement → core}/splitter.py +321 -191
  79. sqlspec/core/statement.py +651 -0
  80. sqlspec/driver/__init__.py +7 -10
  81. sqlspec/driver/_async.py +387 -176
  82. sqlspec/driver/_common.py +527 -289
  83. sqlspec/driver/_sync.py +390 -172
  84. sqlspec/driver/mixins/__init__.py +2 -19
  85. sqlspec/driver/mixins/_result_tools.py +168 -0
  86. sqlspec/driver/mixins/_sql_translator.py +6 -3
  87. sqlspec/exceptions.py +5 -252
  88. sqlspec/extensions/aiosql/adapter.py +93 -96
  89. sqlspec/extensions/litestar/config.py +0 -1
  90. sqlspec/extensions/litestar/handlers.py +15 -26
  91. sqlspec/extensions/litestar/plugin.py +16 -14
  92. sqlspec/extensions/litestar/providers.py +17 -52
  93. sqlspec/loader.py +424 -105
  94. sqlspec/migrations/__init__.py +12 -0
  95. sqlspec/migrations/base.py +92 -68
  96. sqlspec/migrations/commands.py +24 -106
  97. sqlspec/migrations/loaders.py +402 -0
  98. sqlspec/migrations/runner.py +49 -51
  99. sqlspec/migrations/tracker.py +31 -44
  100. sqlspec/migrations/utils.py +64 -24
  101. sqlspec/protocols.py +7 -183
  102. sqlspec/storage/__init__.py +1 -1
  103. sqlspec/storage/backends/base.py +37 -40
  104. sqlspec/storage/backends/fsspec.py +136 -112
  105. sqlspec/storage/backends/obstore.py +138 -160
  106. sqlspec/storage/capabilities.py +5 -4
  107. sqlspec/storage/registry.py +57 -106
  108. sqlspec/typing.py +136 -115
  109. sqlspec/utils/__init__.py +2 -3
  110. sqlspec/utils/correlation.py +0 -3
  111. sqlspec/utils/deprecation.py +6 -6
  112. sqlspec/utils/fixtures.py +6 -6
  113. sqlspec/utils/logging.py +0 -2
  114. sqlspec/utils/module_loader.py +7 -12
  115. sqlspec/utils/singleton.py +0 -1
  116. sqlspec/utils/sync_tools.py +16 -37
  117. sqlspec/utils/text.py +12 -51
  118. sqlspec/utils/type_guards.py +443 -232
  119. {sqlspec-0.14.0.dist-info → sqlspec-0.15.0.dist-info}/METADATA +7 -2
  120. sqlspec-0.15.0.dist-info/RECORD +134 -0
  121. sqlspec-0.15.0.dist-info/entry_points.txt +2 -0
  122. sqlspec/driver/connection.py +0 -207
  123. sqlspec/driver/mixins/_cache.py +0 -114
  124. sqlspec/driver/mixins/_csv_writer.py +0 -91
  125. sqlspec/driver/mixins/_pipeline.py +0 -508
  126. sqlspec/driver/mixins/_query_tools.py +0 -796
  127. sqlspec/driver/mixins/_result_utils.py +0 -138
  128. sqlspec/driver/mixins/_storage.py +0 -912
  129. sqlspec/driver/mixins/_type_coercion.py +0 -128
  130. sqlspec/driver/parameters.py +0 -138
  131. sqlspec/statement/__init__.py +0 -21
  132. sqlspec/statement/builder/_merge.py +0 -95
  133. sqlspec/statement/cache.py +0 -50
  134. sqlspec/statement/filters.py +0 -625
  135. sqlspec/statement/parameters.py +0 -996
  136. sqlspec/statement/pipelines/__init__.py +0 -210
  137. sqlspec/statement/pipelines/analyzers/__init__.py +0 -9
  138. sqlspec/statement/pipelines/analyzers/_analyzer.py +0 -646
  139. sqlspec/statement/pipelines/context.py +0 -115
  140. sqlspec/statement/pipelines/transformers/__init__.py +0 -7
  141. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +0 -88
  142. sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +0 -1247
  143. sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +0 -76
  144. sqlspec/statement/pipelines/validators/__init__.py +0 -23
  145. sqlspec/statement/pipelines/validators/_dml_safety.py +0 -290
  146. sqlspec/statement/pipelines/validators/_parameter_style.py +0 -370
  147. sqlspec/statement/pipelines/validators/_performance.py +0 -714
  148. sqlspec/statement/pipelines/validators/_security.py +0 -967
  149. sqlspec/statement/result.py +0 -435
  150. sqlspec/statement/sql.py +0 -1774
  151. sqlspec/utils/cached_property.py +0 -25
  152. sqlspec/utils/statement_hashing.py +0 -203
  153. sqlspec-0.14.0.dist-info/RECORD +0 -143
  154. sqlspec-0.14.0.dist-info/entry_points.txt +0 -2
  155. /sqlspec/{statement/builder → builder}/mixins/_delete_operations.py +0 -0
  156. {sqlspec-0.14.0.dist-info → sqlspec-0.15.0.dist-info}/WHEEL +0 -0
  157. {sqlspec-0.14.0.dist-info → sqlspec-0.15.0.dist-info}/licenses/LICENSE +0 -0
  158. {sqlspec-0.14.0.dist-info → sqlspec-0.15.0.dist-info}/licenses/NOTICE +0 -0
@@ -1,16 +1,16 @@
1
1
  """Unified Storage Registry for ObjectStore backends.
2
2
 
3
- This module provides a flexible, lazy-loading storage registry that supports:
4
- - URI-first access pattern with automatic backend detection
5
- - ObStore preferred, FSSpec fallback architecture
6
- - Intelligent scheme-based routing with dependency detection
7
- - Named aliases for commonly used configurations (secondary feature)
8
- - Automatic instrumentation integration
3
+ Provides a flexible, lazy-loading storage registry that supports URI-first access
4
+ pattern with automatic backend detection, ObStore preferred with FSSpec fallback,
5
+ intelligent scheme-based routing, and named aliases for common configurations.
9
6
  """
10
7
 
11
8
  import logging
9
+ import re
12
10
  from pathlib import Path
13
- from typing import Any, Optional, TypeVar, Union, cast
11
+ from typing import Any, Final, Optional, Union, cast
12
+
13
+ from mypy_extensions import mypyc_attr
14
14
 
15
15
  from sqlspec.exceptions import ImproperConfigurationError, MissingDependencyError
16
16
  from sqlspec.protocols import ObjectStoreProtocol
@@ -21,28 +21,27 @@ __all__ = ("StorageRegistry", "storage_registry")
21
21
 
22
22
  logger = logging.getLogger(__name__)
23
23
 
24
- BackendT = TypeVar("BackendT", bound=ObjectStoreProtocol)
25
24
 
26
- FSSPEC_ONLY_SCHEMES = {"http", "https", "ftp", "sftp", "ssh"}
25
+ SCHEME_REGEX: Final = re.compile(r"([a-zA-Z0-9+.-]+)://")
26
+ FILE_PROTOCOL: Final[str] = "file"
27
+ S3_PROTOCOL: Final[str] = "s3"
28
+ GCS_PROTOCOL: Final[str] = "gs"
29
+ AZURE_PROTOCOL: Final[str] = "az"
30
+ FSSPEC_ONLY_SCHEMES: Final[frozenset[str]] = frozenset({"http", "https", "ftp", "sftp", "ssh"})
27
31
 
28
32
 
33
+ @mypyc_attr(allow_interpreted_subclasses=True)
29
34
  class StorageRegistry:
30
- """Unified storage registry with URI-first access and intelligent backend selection.
35
+ """Storage registry with URI-first access and automatic backend selection.
31
36
 
32
- This registry implements Phase 3 of the unified storage redesign:
33
- - URI-first access pattern - pass URIs directly to get()
34
- - Automatic ObStore preference when available
35
- - Intelligent FSSpec fallback for unsupported schemes or when ObStore unavailable
36
- - Named aliases as secondary feature for commonly used configurations
37
- - Dependency-aware backend selection with clear error messages
37
+ Provides URI-first access pattern with automatic backend selection.
38
+ Named aliases support complex configurations.
38
39
 
39
40
  Examples:
40
- # Primary usage: Direct URI access (no registration needed)
41
- backend = registry.get("s3://my-bucket/file.parquet") # ObStore preferred
42
- backend = registry.get("file:///tmp/data.csv") # Obstore for local files
43
- backend = registry.get("gs://bucket/data.json") # ObStore for GCS
41
+ backend = registry.get("s3://my-bucket/file.parquet")
42
+ backend = registry.get("file:///tmp/data.csv")
43
+ backend = registry.get("gs://bucket/data.json")
44
44
 
45
- # Secondary usage: Named aliases for complex configurations
46
45
  registry.register_alias(
47
46
  "production-s3",
48
47
  uri="s3://prod-bucket/data",
@@ -50,17 +49,16 @@ class StorageRegistry:
50
49
  aws_access_key_id="...",
51
50
  aws_secret_access_key="..."
52
51
  )
53
- backend = registry.get("production-s3") # Uses alias
54
-
55
- # Automatic fallback when ObStore unavailable
56
- # If obstore not installed: s3:// → FSSpec automatically
57
- # Clear error if neither backend supports the scheme
52
+ backend = registry.get("production-s3")
58
53
  """
59
54
 
55
+ __slots__ = ("_alias_configs", "_aliases", "_cache", "_instances")
56
+
60
57
  def __init__(self) -> None:
61
58
  self._alias_configs: dict[str, tuple[type[ObjectStoreProtocol], str, dict[str, Any]]] = {}
62
59
  self._aliases: dict[str, dict[str, Any]] = {}
63
60
  self._instances: dict[Union[str, tuple[str, tuple[tuple[str, Any], ...]]], ObjectStoreProtocol] = {}
61
+ self._cache: dict[str, tuple[str, type[ObjectStoreProtocol]]] = {}
64
62
 
65
63
  def register_alias(
66
64
  self,
@@ -87,26 +85,23 @@ class StorageRegistry:
87
85
 
88
86
  config = config or {}
89
87
  config.update(kwargs)
90
-
91
88
  backend_config = dict(config)
92
89
  if base_path:
93
90
  backend_config["base_path"] = base_path
94
-
95
91
  self._alias_configs[alias] = (backend, uri, backend_config)
96
-
97
92
  test_config = dict(backend_config)
98
93
  test_config["uri"] = uri
99
94
  self._aliases[alias] = test_config
100
95
 
101
96
  def get(self, uri_or_alias: Union[str, Path], **kwargs: Any) -> ObjectStoreProtocol:
102
- """Get backend instance using URI-first routing with intelligent backend selection.
97
+ """Get backend instance using URI-first routing with automatic backend selection.
103
98
 
104
99
  Args:
105
- uri_or_alias: URI to resolve directly OR named alias (secondary feature)
100
+ uri_or_alias: URI to resolve directly OR named alias
106
101
  **kwargs: Additional backend-specific configuration options
107
102
 
108
103
  Returns:
109
- Backend instance with automatic ObStore preference and FSSpec fallback
104
+ Backend instance with automatic backend selection
110
105
 
111
106
  Raises:
112
107
  ImproperConfigurationError: If alias not found or invalid input
@@ -121,83 +116,58 @@ class StorageRegistry:
121
116
  cache_key = (uri_or_alias, tuple(sorted(kwargs.items()))) if kwargs else uri_or_alias
122
117
  if cache_key in self._instances:
123
118
  return self._instances[cache_key]
124
-
125
- if "://" in uri_or_alias:
119
+ scheme = self._get_scheme(uri_or_alias)
120
+ if not scheme and (
121
+ Path(uri_or_alias).exists()
122
+ or Path(uri_or_alias).is_absolute()
123
+ or uri_or_alias.startswith(("~", "."))
124
+ or ":\\" in uri_or_alias
125
+ or "/" in uri_or_alias
126
+ ):
127
+ scheme = "file"
128
+ uri_or_alias = f"file://{uri_or_alias}"
129
+
130
+ if scheme:
126
131
  instance = self._resolve_from_uri(uri_or_alias, **kwargs)
127
132
  elif uri_or_alias in self._alias_configs:
128
133
  backend_cls, stored_uri, config = self._alias_configs[uri_or_alias]
129
- merged_config = {**config, **kwargs}
130
- instance = backend_cls(stored_uri, **merged_config)
134
+ instance = backend_cls(stored_uri, **{**config, **kwargs})
131
135
  else:
132
136
  msg = f"Unknown storage alias or invalid URI: '{uri_or_alias}'"
133
137
  raise ImproperConfigurationError(msg)
134
-
135
138
  self._instances[cache_key] = instance
136
139
  return instance
137
140
 
138
141
  def _resolve_from_uri(self, uri: str, **kwargs: Any) -> ObjectStoreProtocol:
139
142
  """Resolve backend from URI, trying ObStore first, then FSSpec."""
140
143
  scheme = self._get_scheme(uri)
141
- last_exc: Optional[Exception] = None
142
-
143
144
  if scheme not in FSSPEC_ONLY_SCHEMES and OBSTORE_INSTALLED:
144
145
  try:
145
146
  return self._create_backend("obstore", uri, **kwargs)
146
- except (ImportError, ValueError) as e:
147
- logger.debug("ObStore backend failed for %s: %s", uri, e)
148
- last_exc = e
149
-
147
+ except (ValueError, ImportError, NotImplementedError):
148
+ pass
150
149
  if FSSPEC_INSTALLED:
151
150
  try:
152
151
  return self._create_backend("fsspec", uri, **kwargs)
153
- except (ImportError, ValueError) as e:
154
- logger.debug("FSSpec backend failed for %s: %s", uri, e)
155
- last_exc = e
156
-
157
- msg = f"No storage backend available for URI '{uri}'. Install 'obstore' or 'fsspec' and required dependencies."
158
- raise MissingDependencyError(msg) from last_exc
152
+ except (ValueError, ImportError, NotImplementedError):
153
+ pass
154
+ msg = f"No storage backend available for scheme '{scheme}'. Install obstore or fsspec."
155
+ raise MissingDependencyError(msg)
159
156
 
160
157
  def _determine_backend_class(self, uri: str) -> type[ObjectStoreProtocol]:
161
- """Determine the best backend class for a URI based on availability and capabilities.
162
-
163
- Prefers ObStore for its superior performance and native capabilities,
164
- falls back to FSSpec for extended protocol support.
165
-
166
- Args:
167
- uri: URI to determine backend for.
168
-
169
- Returns:
170
- Backend class (not instance)
171
- """
158
+ """Determine the backend class for a URI based on availability."""
172
159
  scheme = self._get_scheme(uri)
173
-
174
- # Check if scheme requires FSSpec (not supported by ObStore)
175
160
  if scheme in FSSPEC_ONLY_SCHEMES and FSSPEC_INSTALLED:
176
161
  return self._get_backend_class("fsspec")
177
-
178
- # Prefer ObStore for its superior performance
179
162
  if OBSTORE_INSTALLED:
180
163
  return self._get_backend_class("obstore")
181
- # Could check capabilities here if needed
182
-
183
164
  if FSSPEC_INSTALLED:
184
165
  return self._get_backend_class("fsspec")
185
-
186
166
  msg = f"No backend available for URI scheme '{scheme}'. Install obstore or fsspec."
187
167
  raise MissingDependencyError(msg)
188
168
 
189
169
  def _get_backend_class(self, backend_type: str) -> type[ObjectStoreProtocol]:
190
- """Get backend class by type name.
191
-
192
- Args:
193
- backend_type: Backend type ('obstore' or 'fsspec')
194
-
195
- Returns:
196
- Backend class
197
-
198
- Raises:
199
- ValueError: If unknown backend type
200
- """
170
+ """Get backend class by type name."""
201
171
  if backend_type == "obstore":
202
172
  from sqlspec.storage.backends.obstore import ObStoreBackend
203
173
 
@@ -211,16 +181,15 @@ class StorageRegistry:
211
181
 
212
182
  def _create_backend(self, backend_type: str, uri: str, **kwargs: Any) -> ObjectStoreProtocol:
213
183
  """Create backend instance for URI."""
214
- backend_cls = self._get_backend_class(backend_type)
215
- return backend_cls(uri, **kwargs)
184
+ return self._get_backend_class(backend_type)(uri, **kwargs)
216
185
 
217
- def _get_scheme(self, uri: str) -> str:
218
- """Extract scheme from URI."""
219
- if not uri or "://" not in uri:
220
- return "file"
221
- return uri.split("://", maxsplit=1)[0].lower()
186
+ def _get_scheme(self, uri: str) -> Optional[str]:
187
+ """Extract the scheme from a URI using regex."""
188
+ if not uri:
189
+ return None
190
+ match = SCHEME_REGEX.match(uri)
191
+ return match.group(1).lower() if match else None
222
192
 
223
- # Utility methods
224
193
  def is_alias_registered(self, alias: str) -> bool:
225
194
  """Check if a named alias is registered."""
226
195
  return alias in self._alias_configs
@@ -230,11 +199,7 @@ class StorageRegistry:
230
199
  return list(self._alias_configs.keys())
231
200
 
232
201
  def clear_cache(self, uri_or_alias: Optional[str] = None) -> None:
233
- """Clear resolved backend cache.
234
-
235
- Args:
236
- uri_or_alias: Specific URI or alias to clear, or None to clear all
237
- """
202
+ """Clear resolved backend cache."""
238
203
  if uri_or_alias:
239
204
  self._instances.pop(uri_or_alias, None)
240
205
  else:
@@ -256,17 +221,9 @@ class StorageRegistry:
256
221
  self._aliases.clear()
257
222
 
258
223
  def get_backend_capabilities(self, uri_or_alias: Union[str, Path]) -> "StorageCapabilities":
259
- """Get capabilities for a backend without creating an instance.
260
-
261
- Args:
262
- uri_or_alias: URI or alias to check capabilities for
263
-
264
- Returns:
265
- StorageCapabilities object describing backend capabilities
266
- """
224
+ """Get capabilities for a backend without creating an instance."""
267
225
  if isinstance(uri_or_alias, Path):
268
226
  uri_or_alias = f"file://{uri_or_alias.resolve()}"
269
-
270
227
  if "://" in uri_or_alias:
271
228
  backend_cls = self._determine_backend_class(uri_or_alias)
272
229
  elif uri_or_alias in self._alias_configs:
@@ -274,15 +231,9 @@ class StorageRegistry:
274
231
  else:
275
232
  msg = f"Unknown storage alias or invalid URI: '{uri_or_alias}'"
276
233
  raise ImproperConfigurationError(msg)
277
-
278
- # Get capabilities from the backend class
279
234
  if hasattr(backend_cls, "capabilities"):
280
235
  return backend_cls.capabilities
281
-
282
- # Default capabilities if not defined
283
-
284
236
  return StorageCapabilities()
285
237
 
286
238
 
287
- # Global registry instance
288
239
  storage_registry = StorageRegistry()
sqlspec/typing.py CHANGED
@@ -1,12 +1,14 @@
1
1
  # pyright: ignore[reportAttributeAccessIssue]
2
- from collections.abc import Mapping
2
+ from collections.abc import Iterator, Mapping
3
3
  from functools import lru_cache
4
- from typing import TYPE_CHECKING, Annotated, Any, Union
4
+ from typing import TYPE_CHECKING, Annotated, Any, Protocol, Union
5
5
 
6
6
  from typing_extensions import TypeAlias, TypeVar
7
7
 
8
8
  from sqlspec._typing import (
9
9
  AIOSQL_INSTALLED,
10
+ ATTRS_INSTALLED,
11
+ CATTRS_INSTALLED,
10
12
  FSSPEC_INSTALLED,
11
13
  LITESTAR_INSTALLED,
12
14
  MSGSPEC_INSTALLED,
@@ -16,74 +18,141 @@ from sqlspec._typing import (
16
18
  PROMETHEUS_INSTALLED,
17
19
  PYARROW_INSTALLED,
18
20
  PYDANTIC_INSTALLED,
19
- UNSET,
20
- AiosqlAsyncProtocol, # pyright: ignore[reportAttributeAccessIssue]
21
- AiosqlParamType, # pyright: ignore[reportAttributeAccessIssue]
22
- AiosqlProtocol, # pyright: ignore[reportAttributeAccessIssue]
23
- AiosqlSQLOperationType, # pyright: ignore[reportAttributeAccessIssue]
24
- AiosqlSyncProtocol, # pyright: ignore[reportAttributeAccessIssue]
25
- ArrowRecordBatch,
26
- ArrowTable,
27
- BaseModel,
28
- Counter, # pyright: ignore[reportAttributeAccessIssue]
29
21
  DataclassProtocol,
30
- DTOData,
31
22
  Empty,
23
+ EmptyEnum,
32
24
  EmptyType,
33
- Gauge, # pyright: ignore[reportAttributeAccessIssue]
34
- Histogram, # pyright: ignore[reportAttributeAccessIssue]
35
- Span, # pyright: ignore[reportAttributeAccessIssue]
36
- Status, # pyright: ignore[reportAttributeAccessIssue]
37
- StatusCode, # pyright: ignore[reportAttributeAccessIssue]
38
- Struct,
39
- Tracer, # pyright: ignore[reportAttributeAccessIssue]
40
- TypeAdapter,
41
- UnsetType,
42
- aiosql,
43
- convert, # pyright: ignore[reportAttributeAccessIssue]
44
- trace,
45
25
  )
46
26
 
47
27
  if TYPE_CHECKING:
48
28
  from collections.abc import Sequence
49
29
 
30
+ from sqlspec._typing import (
31
+ UNSET,
32
+ AiosqlAsyncProtocol,
33
+ AiosqlParamType,
34
+ AiosqlProtocol,
35
+ AiosqlSQLOperationType,
36
+ AiosqlSyncProtocol,
37
+ ArrowRecordBatch,
38
+ ArrowTable,
39
+ AttrsInstance,
40
+ AttrsInstanceStub,
41
+ BaseModel,
42
+ BaseModelStub,
43
+ Counter,
44
+ DTOData,
45
+ FailFast,
46
+ Gauge,
47
+ Histogram,
48
+ Span,
49
+ Status,
50
+ StatusCode,
51
+ Struct,
52
+ StructStub,
53
+ Tracer,
54
+ TypeAdapter,
55
+ UnsetType,
56
+ aiosql,
57
+ attrs_asdict,
58
+ attrs_define,
59
+ attrs_field,
60
+ attrs_fields,
61
+ attrs_has,
62
+ cattrs_structure,
63
+ cattrs_unstructure,
64
+ convert,
65
+ trace,
66
+ )
67
+ else:
68
+ from sqlspec._typing import (
69
+ UNSET,
70
+ AiosqlAsyncProtocol,
71
+ AiosqlParamType,
72
+ AiosqlProtocol,
73
+ AiosqlSQLOperationType,
74
+ AiosqlSyncProtocol,
75
+ ArrowRecordBatch,
76
+ ArrowTable,
77
+ AttrsInstance,
78
+ BaseModel,
79
+ Counter,
80
+ DTOData,
81
+ FailFast,
82
+ Gauge,
83
+ Histogram,
84
+ Span,
85
+ Status,
86
+ StatusCode,
87
+ Struct,
88
+ Tracer,
89
+ TypeAdapter,
90
+ UnsetType,
91
+ aiosql,
92
+ attrs_asdict,
93
+ attrs_define,
94
+ attrs_field,
95
+ attrs_fields,
96
+ attrs_has,
97
+ cattrs_structure,
98
+ cattrs_unstructure,
99
+ convert,
100
+ trace,
101
+ )
102
+
103
+
104
+ class DictLike(Protocol):
105
+ """A protocol for objects that behave like a dictionary for reading."""
106
+
107
+ def __getitem__(self, key: str) -> Any: ...
108
+ def __iter__(self) -> Iterator[str]: ...
109
+ def __len__(self) -> int: ...
110
+
111
+
112
+ PYDANTIC_USE_FAILFAST = False
50
113
 
51
- PYDANTIC_USE_FAILFAST = False # leave permanently disabled for now
52
114
 
115
+ if TYPE_CHECKING:
116
+ T = TypeVar("T")
117
+ ConnectionT = TypeVar("ConnectionT")
118
+ """Type variable for connection types.
53
119
 
54
- T = TypeVar("T")
55
- ConnectionT = TypeVar("ConnectionT")
56
- """Type variable for connection types.
57
-
58
- :class:`~sqlspec.typing.ConnectionT`
59
- """
60
- PoolT = TypeVar("PoolT")
61
- """Type variable for pool types.
120
+ :class:`~sqlspec.typing.ConnectionT`
121
+ """
122
+ PoolT = TypeVar("PoolT")
123
+ """Type variable for pool types.
62
124
 
63
- :class:`~sqlspec.typing.PoolT`
64
- """
65
- PoolT_co = TypeVar("PoolT_co", covariant=True)
66
- """Type variable for covariant pool types.
125
+ :class:`~sqlspec.typing.PoolT`
126
+ """
127
+ PoolT_co = TypeVar("PoolT_co", covariant=True)
128
+ """Type variable for covariant pool types.
67
129
 
68
- :class:`~sqlspec.typing.PoolT_co`
69
- """
70
- ModelT = TypeVar("ModelT", bound="Union[dict[str, Any], Struct, BaseModel, DataclassProtocol]")
71
- """Type variable for model types.
130
+ :class:`~sqlspec.typing.PoolT_co`
131
+ """
132
+ ModelT = TypeVar("ModelT", bound="Union[DictLike, StructStub, BaseModelStub, DataclassProtocol, AttrsInstanceStub]")
133
+ """Type variable for model types.
72
134
 
73
- :class:`dict[str, Any]` | :class:`msgspec.Struct` | :class:`pydantic.BaseModel` | :class:`DataclassProtocol`
74
- """
135
+ :class:`DictLike` | :class:`msgspec.Struct` | :class:`pydantic.BaseModel` | :class:`DataclassProtocol` | :class:`AttrsInstance`
136
+ """
137
+ RowT = TypeVar("RowT", bound="dict[str, Any]")
138
+ else:
139
+ T = Any
140
+ ConnectionT = Any
141
+ PoolT = Any
142
+ PoolT_co = Any
143
+ ModelT = Any
144
+ RowT = dict[str, Any]
75
145
 
76
146
 
77
147
  DictRow: TypeAlias = "dict[str, Any]"
78
148
  """Type variable for DictRow types."""
79
149
  TupleRow: TypeAlias = "tuple[Any, ...]"
80
150
  """Type variable for TupleRow types."""
81
- RowT = TypeVar("RowT", default=dict[str, Any])
82
151
 
83
- SupportedSchemaModel: TypeAlias = "Union[Struct, BaseModel, DataclassProtocol]"
152
+ SupportedSchemaModel: TypeAlias = "Union[DictLike, StructStub, BaseModelStub, DataclassProtocol, AttrsInstanceStub]"
84
153
  """Type alias for pydantic or msgspec models.
85
154
 
86
- :class:`msgspec.Struct` | :class:`pydantic.BaseModel` | :class:`DataclassProtocol`
155
+ :class:`msgspec.Struct` | :class:`pydantic.BaseModel` | :class:`DataclassProtocol` | :class:`AttrsInstance`
87
156
  """
88
157
  StatementParameters: TypeAlias = "Union[Any, dict[str, Any], list[Any], tuple[Any, ...], None]"
89
158
  """Type alias for statement parameters.
@@ -94,8 +163,6 @@ Represents:
94
163
  - :type:`tuple[Any, ...]`
95
164
  - :type:`None`
96
165
  """
97
- # Backward compatibility alias
98
- SQLParameterType: TypeAlias = StatementParameters
99
166
  ModelDTOT = TypeVar("ModelDTOT", bound="SupportedSchemaModel")
100
167
  """Type variable for model DTOs.
101
168
 
@@ -106,22 +173,24 @@ PydanticOrMsgspecT = SupportedSchemaModel
106
173
 
107
174
  :class:`msgspec.Struct` or :class:`pydantic.BaseModel`
108
175
  """
109
- ModelDict: TypeAlias = "Union[dict[str, Any], SupportedSchemaModel, DTOData[SupportedSchemaModel]]"
176
+ ModelDict: TypeAlias = (
177
+ "Union[dict[str, Any], Union[DictLike, StructStub, BaseModelStub, DataclassProtocol, AttrsInstanceStub], Any]"
178
+ )
110
179
  """Type alias for model dictionaries.
111
180
 
112
181
  Represents:
113
182
  - :type:`dict[str, Any]` | :class:`DataclassProtocol` | :class:`msgspec.Struct` | :class:`pydantic.BaseModel`
114
183
  """
115
- ModelDictList: TypeAlias = "Sequence[Union[dict[str, Any], SupportedSchemaModel]]"
184
+ ModelDictList: TypeAlias = (
185
+ "Sequence[Union[dict[str, Any], Union[DictLike, StructStub, BaseModelStub, DataclassProtocol, AttrsInstanceStub]]]"
186
+ )
116
187
  """Type alias for model dictionary lists.
117
188
 
118
189
  A list or sequence of any of the following:
119
190
  - :type:`Sequence`[:type:`dict[str, Any]` | :class:`DataclassProtocol` | :class:`msgspec.Struct` | :class:`pydantic.BaseModel`]
120
191
 
121
192
  """
122
- BulkModelDict: TypeAlias = (
123
- "Union[Sequence[Union[dict[str, Any], SupportedSchemaModel]], DTOData[list[SupportedSchemaModel]]]"
124
- )
193
+ BulkModelDict: TypeAlias = "Union[Sequence[Union[dict[str, Any], Union[DictLike, StructStub, BaseModelStub, DataclassProtocol, AttrsInstanceStub]]], Any]"
125
194
  """Type alias for bulk model dictionaries.
126
195
 
127
196
  Represents:
@@ -131,7 +200,7 @@ Represents:
131
200
 
132
201
 
133
202
  @lru_cache(typed=True)
134
- def get_type_adapter(f: "type[T]") -> "TypeAdapter[T]":
203
+ def get_type_adapter(f: "type[T]") -> Any:
135
204
  """Caches and returns a pydantic type adapter.
136
205
 
137
206
  Args:
@@ -159,6 +228,8 @@ def MixinOf(base: type[T]) -> type[T]: # noqa: N802
159
228
 
160
229
  __all__ = (
161
230
  "AIOSQL_INSTALLED",
231
+ "ATTRS_INSTALLED",
232
+ "CATTRS_INSTALLED",
162
233
  "FSSPEC_INSTALLED",
163
234
  "LITESTAR_INSTALLED",
164
235
  "MSGSPEC_INSTALLED",
@@ -177,14 +248,17 @@ __all__ = (
177
248
  "AiosqlSyncProtocol",
178
249
  "ArrowRecordBatch",
179
250
  "ArrowTable",
251
+ "AttrsInstance",
180
252
  "BaseModel",
181
253
  "BulkModelDict",
182
254
  "ConnectionT",
183
255
  "Counter",
184
256
  "DTOData",
185
257
  "DataclassProtocol",
258
+ "DictLike",
186
259
  "DictRow",
187
260
  "Empty",
261
+ "EmptyEnum",
188
262
  "EmptyType",
189
263
  "FailFast",
190
264
  "Gauge",
@@ -201,7 +275,6 @@ __all__ = (
201
275
  "PoolT_co",
202
276
  "PydanticOrMsgspecT",
203
277
  "RowT",
204
- "SQLParameterType",
205
278
  "Span",
206
279
  "StatementParameters",
207
280
  "Status",
@@ -213,66 +286,14 @@ __all__ = (
213
286
  "TypeAdapter",
214
287
  "UnsetType",
215
288
  "aiosql",
289
+ "attrs_asdict",
290
+ "attrs_define",
291
+ "attrs_field",
292
+ "attrs_fields",
293
+ "attrs_has",
294
+ "cattrs_structure",
295
+ "cattrs_unstructure",
216
296
  "convert",
217
297
  "get_type_adapter",
218
298
  "trace",
219
299
  )
220
-
221
- if TYPE_CHECKING:
222
- if not PYDANTIC_INSTALLED:
223
- from sqlspec._typing import BaseModel, FailFast, TypeAdapter
224
- else:
225
- from pydantic import BaseModel, FailFast, TypeAdapter # noqa: TC004
226
-
227
- if not MSGSPEC_INSTALLED:
228
- from sqlspec._typing import UNSET, Struct, UnsetType, convert
229
- else:
230
- from msgspec import UNSET, Struct, UnsetType, convert # noqa: TC004
231
-
232
- if not PYARROW_INSTALLED:
233
- from sqlspec._typing import ArrowRecordBatch, ArrowTable
234
- else:
235
- from pyarrow import RecordBatch as ArrowRecordBatch # noqa: TC004
236
- from pyarrow import Table as ArrowTable # noqa: TC004
237
- if not LITESTAR_INSTALLED:
238
- from sqlspec._typing import DTOData
239
- else:
240
- from litestar.dto import DTOData # noqa: TC004
241
- if not OPENTELEMETRY_INSTALLED:
242
- from sqlspec._typing import Span, Status, StatusCode, Tracer, trace # noqa: TC004 # pyright: ignore
243
- else:
244
- from opentelemetry.trace import ( # pyright: ignore[reportMissingImports] # noqa: TC004
245
- Span,
246
- Status,
247
- StatusCode,
248
- Tracer,
249
- )
250
- if not PROMETHEUS_INSTALLED:
251
- from sqlspec._typing import Counter, Gauge, Histogram # pyright: ignore
252
- else:
253
- from prometheus_client import Counter, Gauge, Histogram # noqa: TC004 # pyright: ignore # noqa: TC004
254
-
255
- if not AIOSQL_INSTALLED:
256
- from sqlspec._typing import (
257
- AiosqlAsyncProtocol, # pyright: ignore[reportAttributeAccessIssue]
258
- AiosqlParamType, # pyright: ignore[reportAttributeAccessIssue]
259
- AiosqlProtocol, # pyright: ignore[reportAttributeAccessIssue]
260
- AiosqlSQLOperationType, # pyright: ignore[reportAttributeAccessIssue]
261
- AiosqlSyncProtocol, # pyright: ignore[reportAttributeAccessIssue]
262
- aiosql,
263
- )
264
- else:
265
- import aiosql # noqa: TC004 # pyright: ignore
266
- from aiosql.types import ( # noqa: TC004 # pyright: ignore[reportMissingImports]
267
- AsyncDriverAdapterProtocol as AiosqlAsyncProtocol,
268
- )
269
- from aiosql.types import ( # noqa: TC004 # pyright: ignore[reportMissingImports]
270
- DriverAdapterProtocol as AiosqlProtocol,
271
- )
272
- from aiosql.types import ParamType as AiosqlParamType # noqa: TC004 # pyright: ignore[reportMissingImports]
273
- from aiosql.types import (
274
- SQLOperationType as AiosqlSQLOperationType, # noqa: TC004 # pyright: ignore[reportMissingImports]
275
- )
276
- from aiosql.types import ( # noqa: TC004 # pyright: ignore[reportMissingImports]
277
- SyncDriverAdapterProtocol as AiosqlSyncProtocol,
278
- )
sqlspec/utils/__init__.py CHANGED
@@ -1,4 +1,3 @@
1
- from sqlspec.utils import deprecation, fixtures, module_loader, singleton, sync_tools, text
2
- from sqlspec.utils.statement_hashing import hash_expression
1
+ from sqlspec.utils import deprecation, fixtures, module_loader, singleton, sync_tools, text, type_guards
3
2
 
4
- __all__ = ("deprecation", "fixtures", "hash_expression", "module_loader", "singleton", "sync_tools", "text")
3
+ __all__ = ("deprecation", "fixtures", "module_loader", "singleton", "sync_tools", "text", "type_guards")
@@ -68,14 +68,12 @@ class CorrelationContext:
68
68
  if correlation_id is None:
69
69
  correlation_id = cls.generate()
70
70
 
71
- # Save the current correlation ID
72
71
  previous_id = cls.get()
73
72
 
74
73
  try:
75
74
  cls.set(correlation_id)
76
75
  yield correlation_id
77
76
  finally:
78
- # Restore the previous correlation ID
79
77
  cls.set(previous_id)
80
78
 
81
79
  @classmethod
@@ -111,7 +109,6 @@ def correlation_context(correlation_id: str | None = None) -> Generator[str, Non
111
109
  "Processing request",
112
110
  extra={"correlation_id": correlation_id},
113
111
  )
114
- # All operations within this context will have the same correlation ID
115
112
  ```
116
113
  """
117
114
  with CorrelationContext.context(correlation_id) as cid: