sqlspec 0.26.0__py3-none-any.whl → 0.27.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 (197) hide show
  1. sqlspec/__init__.py +7 -15
  2. sqlspec/_serialization.py +55 -25
  3. sqlspec/_typing.py +62 -52
  4. sqlspec/adapters/adbc/_types.py +1 -1
  5. sqlspec/adapters/adbc/adk/__init__.py +5 -0
  6. sqlspec/adapters/adbc/adk/store.py +870 -0
  7. sqlspec/adapters/adbc/config.py +62 -12
  8. sqlspec/adapters/adbc/data_dictionary.py +52 -2
  9. sqlspec/adapters/adbc/driver.py +144 -45
  10. sqlspec/adapters/adbc/litestar/__init__.py +5 -0
  11. sqlspec/adapters/adbc/litestar/store.py +504 -0
  12. sqlspec/adapters/adbc/type_converter.py +44 -50
  13. sqlspec/adapters/aiosqlite/_types.py +1 -1
  14. sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
  15. sqlspec/adapters/aiosqlite/adk/store.py +527 -0
  16. sqlspec/adapters/aiosqlite/config.py +86 -16
  17. sqlspec/adapters/aiosqlite/data_dictionary.py +34 -2
  18. sqlspec/adapters/aiosqlite/driver.py +127 -38
  19. sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
  20. sqlspec/adapters/aiosqlite/litestar/store.py +281 -0
  21. sqlspec/adapters/aiosqlite/pool.py +7 -7
  22. sqlspec/adapters/asyncmy/__init__.py +7 -1
  23. sqlspec/adapters/asyncmy/_types.py +1 -1
  24. sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
  25. sqlspec/adapters/asyncmy/adk/store.py +493 -0
  26. sqlspec/adapters/asyncmy/config.py +59 -17
  27. sqlspec/adapters/asyncmy/data_dictionary.py +41 -2
  28. sqlspec/adapters/asyncmy/driver.py +293 -62
  29. sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
  30. sqlspec/adapters/asyncmy/litestar/store.py +296 -0
  31. sqlspec/adapters/asyncpg/__init__.py +2 -1
  32. sqlspec/adapters/asyncpg/_type_handlers.py +71 -0
  33. sqlspec/adapters/asyncpg/_types.py +11 -7
  34. sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
  35. sqlspec/adapters/asyncpg/adk/store.py +450 -0
  36. sqlspec/adapters/asyncpg/config.py +57 -36
  37. sqlspec/adapters/asyncpg/data_dictionary.py +41 -2
  38. sqlspec/adapters/asyncpg/driver.py +153 -23
  39. sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
  40. sqlspec/adapters/asyncpg/litestar/store.py +253 -0
  41. sqlspec/adapters/bigquery/_types.py +1 -1
  42. sqlspec/adapters/bigquery/adk/__init__.py +5 -0
  43. sqlspec/adapters/bigquery/adk/store.py +576 -0
  44. sqlspec/adapters/bigquery/config.py +25 -11
  45. sqlspec/adapters/bigquery/data_dictionary.py +42 -2
  46. sqlspec/adapters/bigquery/driver.py +352 -144
  47. sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
  48. sqlspec/adapters/bigquery/litestar/store.py +327 -0
  49. sqlspec/adapters/bigquery/type_converter.py +55 -23
  50. sqlspec/adapters/duckdb/_types.py +2 -2
  51. sqlspec/adapters/duckdb/adk/__init__.py +14 -0
  52. sqlspec/adapters/duckdb/adk/store.py +553 -0
  53. sqlspec/adapters/duckdb/config.py +79 -21
  54. sqlspec/adapters/duckdb/data_dictionary.py +41 -2
  55. sqlspec/adapters/duckdb/driver.py +138 -43
  56. sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
  57. sqlspec/adapters/duckdb/litestar/store.py +332 -0
  58. sqlspec/adapters/duckdb/pool.py +5 -5
  59. sqlspec/adapters/duckdb/type_converter.py +51 -21
  60. sqlspec/adapters/oracledb/_numpy_handlers.py +133 -0
  61. sqlspec/adapters/oracledb/_types.py +20 -2
  62. sqlspec/adapters/oracledb/adk/__init__.py +5 -0
  63. sqlspec/adapters/oracledb/adk/store.py +1745 -0
  64. sqlspec/adapters/oracledb/config.py +120 -36
  65. sqlspec/adapters/oracledb/data_dictionary.py +87 -20
  66. sqlspec/adapters/oracledb/driver.py +292 -84
  67. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  68. sqlspec/adapters/oracledb/litestar/store.py +767 -0
  69. sqlspec/adapters/oracledb/migrations.py +316 -25
  70. sqlspec/adapters/oracledb/type_converter.py +91 -16
  71. sqlspec/adapters/psqlpy/_type_handlers.py +44 -0
  72. sqlspec/adapters/psqlpy/_types.py +2 -1
  73. sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
  74. sqlspec/adapters/psqlpy/adk/store.py +482 -0
  75. sqlspec/adapters/psqlpy/config.py +45 -19
  76. sqlspec/adapters/psqlpy/data_dictionary.py +41 -2
  77. sqlspec/adapters/psqlpy/driver.py +101 -31
  78. sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
  79. sqlspec/adapters/psqlpy/litestar/store.py +272 -0
  80. sqlspec/adapters/psqlpy/type_converter.py +40 -11
  81. sqlspec/adapters/psycopg/_type_handlers.py +80 -0
  82. sqlspec/adapters/psycopg/_types.py +2 -1
  83. sqlspec/adapters/psycopg/adk/__init__.py +5 -0
  84. sqlspec/adapters/psycopg/adk/store.py +944 -0
  85. sqlspec/adapters/psycopg/config.py +65 -37
  86. sqlspec/adapters/psycopg/data_dictionary.py +77 -3
  87. sqlspec/adapters/psycopg/driver.py +200 -78
  88. sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
  89. sqlspec/adapters/psycopg/litestar/store.py +554 -0
  90. sqlspec/adapters/sqlite/__init__.py +2 -1
  91. sqlspec/adapters/sqlite/_type_handlers.py +86 -0
  92. sqlspec/adapters/sqlite/_types.py +1 -1
  93. sqlspec/adapters/sqlite/adk/__init__.py +5 -0
  94. sqlspec/adapters/sqlite/adk/store.py +572 -0
  95. sqlspec/adapters/sqlite/config.py +85 -16
  96. sqlspec/adapters/sqlite/data_dictionary.py +34 -2
  97. sqlspec/adapters/sqlite/driver.py +120 -52
  98. sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
  99. sqlspec/adapters/sqlite/litestar/store.py +318 -0
  100. sqlspec/adapters/sqlite/pool.py +5 -5
  101. sqlspec/base.py +45 -26
  102. sqlspec/builder/__init__.py +73 -4
  103. sqlspec/builder/_base.py +91 -58
  104. sqlspec/builder/_column.py +5 -5
  105. sqlspec/builder/_ddl.py +98 -89
  106. sqlspec/builder/_delete.py +5 -4
  107. sqlspec/builder/_dml.py +388 -0
  108. sqlspec/{_sql.py → builder/_factory.py} +41 -44
  109. sqlspec/builder/_insert.py +5 -82
  110. sqlspec/builder/{mixins/_join_operations.py → _join.py} +145 -143
  111. sqlspec/builder/_merge.py +446 -11
  112. sqlspec/builder/_parsing_utils.py +9 -11
  113. sqlspec/builder/_select.py +1313 -25
  114. sqlspec/builder/_update.py +11 -42
  115. sqlspec/cli.py +76 -69
  116. sqlspec/config.py +231 -60
  117. sqlspec/core/__init__.py +5 -4
  118. sqlspec/core/cache.py +18 -18
  119. sqlspec/core/compiler.py +6 -8
  120. sqlspec/core/filters.py +37 -37
  121. sqlspec/core/hashing.py +9 -9
  122. sqlspec/core/parameters.py +76 -45
  123. sqlspec/core/result.py +102 -46
  124. sqlspec/core/splitter.py +16 -17
  125. sqlspec/core/statement.py +32 -31
  126. sqlspec/core/type_conversion.py +3 -2
  127. sqlspec/driver/__init__.py +1 -3
  128. sqlspec/driver/_async.py +95 -161
  129. sqlspec/driver/_common.py +133 -80
  130. sqlspec/driver/_sync.py +95 -162
  131. sqlspec/driver/mixins/_result_tools.py +20 -236
  132. sqlspec/driver/mixins/_sql_translator.py +4 -4
  133. sqlspec/exceptions.py +70 -7
  134. sqlspec/extensions/adk/__init__.py +53 -0
  135. sqlspec/extensions/adk/_types.py +51 -0
  136. sqlspec/extensions/adk/converters.py +172 -0
  137. sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +144 -0
  138. sqlspec/extensions/adk/migrations/__init__.py +0 -0
  139. sqlspec/extensions/adk/service.py +181 -0
  140. sqlspec/extensions/adk/store.py +536 -0
  141. sqlspec/extensions/aiosql/adapter.py +73 -53
  142. sqlspec/extensions/litestar/__init__.py +21 -4
  143. sqlspec/extensions/litestar/cli.py +54 -10
  144. sqlspec/extensions/litestar/config.py +59 -266
  145. sqlspec/extensions/litestar/handlers.py +46 -17
  146. sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
  147. sqlspec/extensions/litestar/migrations/__init__.py +3 -0
  148. sqlspec/extensions/litestar/plugin.py +324 -223
  149. sqlspec/extensions/litestar/providers.py +25 -25
  150. sqlspec/extensions/litestar/store.py +265 -0
  151. sqlspec/loader.py +30 -49
  152. sqlspec/migrations/base.py +200 -76
  153. sqlspec/migrations/commands.py +591 -62
  154. sqlspec/migrations/context.py +6 -9
  155. sqlspec/migrations/fix.py +199 -0
  156. sqlspec/migrations/loaders.py +47 -19
  157. sqlspec/migrations/runner.py +241 -75
  158. sqlspec/migrations/tracker.py +237 -21
  159. sqlspec/migrations/utils.py +51 -3
  160. sqlspec/migrations/validation.py +177 -0
  161. sqlspec/protocols.py +66 -36
  162. sqlspec/storage/_utils.py +98 -0
  163. sqlspec/storage/backends/fsspec.py +134 -106
  164. sqlspec/storage/backends/local.py +78 -51
  165. sqlspec/storage/backends/obstore.py +278 -162
  166. sqlspec/storage/registry.py +75 -39
  167. sqlspec/typing.py +14 -84
  168. sqlspec/utils/config_resolver.py +6 -6
  169. sqlspec/utils/correlation.py +4 -5
  170. sqlspec/utils/data_transformation.py +3 -2
  171. sqlspec/utils/deprecation.py +9 -8
  172. sqlspec/utils/fixtures.py +4 -4
  173. sqlspec/utils/logging.py +46 -6
  174. sqlspec/utils/module_loader.py +2 -2
  175. sqlspec/utils/schema.py +288 -0
  176. sqlspec/utils/serializers.py +3 -3
  177. sqlspec/utils/sync_tools.py +21 -17
  178. sqlspec/utils/text.py +1 -2
  179. sqlspec/utils/type_guards.py +111 -20
  180. sqlspec/utils/version.py +433 -0
  181. {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/METADATA +40 -21
  182. sqlspec-0.27.0.dist-info/RECORD +207 -0
  183. sqlspec/builder/mixins/__init__.py +0 -55
  184. sqlspec/builder/mixins/_cte_and_set_ops.py +0 -253
  185. sqlspec/builder/mixins/_delete_operations.py +0 -50
  186. sqlspec/builder/mixins/_insert_operations.py +0 -282
  187. sqlspec/builder/mixins/_merge_operations.py +0 -698
  188. sqlspec/builder/mixins/_order_limit_operations.py +0 -145
  189. sqlspec/builder/mixins/_pivot_operations.py +0 -157
  190. sqlspec/builder/mixins/_select_operations.py +0 -930
  191. sqlspec/builder/mixins/_update_operations.py +0 -199
  192. sqlspec/builder/mixins/_where_clause.py +0 -1298
  193. sqlspec-0.26.0.dist-info/RECORD +0 -157
  194. sqlspec-0.26.0.dist-info/licenses/NOTICE +0 -29
  195. {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/WHEEL +0 -0
  196. {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/entry_points.txt +0 -0
  197. {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/licenses/LICENSE +0 -0
@@ -7,7 +7,7 @@ Used for loading modules from dotted paths and converting module paths to filesy
7
7
  import importlib
8
8
  from importlib.util import find_spec
9
9
  from pathlib import Path
10
- from typing import Any, Optional
10
+ from typing import Any
11
11
 
12
12
  __all__ = ("import_string", "module_to_os_path")
13
13
 
@@ -46,7 +46,7 @@ def import_string(dotted_path: str) -> "Any":
46
46
  The imported object.
47
47
  """
48
48
 
49
- def _raise_import_error(msg: str, exc: "Optional[Exception]" = None) -> None:
49
+ def _raise_import_error(msg: str, exc: "Exception | None" = None) -> None:
50
50
  if exc is not None:
51
51
  raise ImportError(msg) from exc
52
52
  raise ImportError(msg)
@@ -0,0 +1,288 @@
1
+ """Schema transformation utilities for converting data to various schema types."""
2
+
3
+ import datetime
4
+ import logging
5
+ from collections.abc import Callable, Sequence
6
+ from enum import Enum
7
+ from functools import lru_cache, partial
8
+ from pathlib import Path, PurePath
9
+ from typing import Any, Final, TypeGuard, overload
10
+ from uuid import UUID
11
+
12
+ from typing_extensions import TypeVar
13
+
14
+ from sqlspec.exceptions import SQLSpecError
15
+ from sqlspec.typing import (
16
+ CATTRS_INSTALLED,
17
+ NUMPY_INSTALLED,
18
+ SchemaT,
19
+ attrs_asdict,
20
+ cattrs_structure,
21
+ cattrs_unstructure,
22
+ convert,
23
+ get_type_adapter,
24
+ )
25
+ from sqlspec.utils.data_transformation import transform_dict_keys
26
+ from sqlspec.utils.text import camelize, kebabize, pascalize
27
+ from sqlspec.utils.type_guards import (
28
+ get_msgspec_rename_config,
29
+ is_attrs_schema,
30
+ is_dataclass,
31
+ is_dict,
32
+ is_msgspec_struct,
33
+ is_pydantic_model,
34
+ is_typed_dict,
35
+ )
36
+
37
+ __all__ = (
38
+ "_DEFAULT_TYPE_DECODERS",
39
+ "DataT",
40
+ "_convert_numpy_to_list",
41
+ "_default_msgspec_deserializer",
42
+ "_is_list_type_target",
43
+ "to_schema",
44
+ )
45
+
46
+ DataT = TypeVar("DataT", default=dict[str, Any])
47
+
48
+ logger = logging.getLogger(__name__)
49
+
50
+ _DATETIME_TYPES: Final[set[type]] = {datetime.datetime, datetime.date, datetime.time}
51
+
52
+
53
+ def _is_list_type_target(target_type: Any) -> TypeGuard[list[object]]:
54
+ """Check if target type is a list type (e.g., list[float])."""
55
+ try:
56
+ return hasattr(target_type, "__origin__") and target_type.__origin__ is list
57
+ except (AttributeError, TypeError):
58
+ return False
59
+
60
+
61
+ def _convert_numpy_to_list(target_type: Any, value: Any) -> Any:
62
+ """Convert numpy array to list if target is a list type."""
63
+ if not NUMPY_INSTALLED:
64
+ return value
65
+
66
+ import numpy as np
67
+
68
+ if isinstance(value, np.ndarray) and _is_list_type_target(target_type):
69
+ return value.tolist()
70
+
71
+ return value
72
+
73
+
74
+ @lru_cache(maxsize=128)
75
+ def _detect_schema_type(schema_type: type) -> "str | None":
76
+ """Detect schema type with LRU caching.
77
+
78
+ Args:
79
+ schema_type: Type to detect
80
+
81
+ Returns:
82
+ Type identifier string or None if unsupported
83
+ """
84
+ return (
85
+ "typed_dict"
86
+ if is_typed_dict(schema_type)
87
+ else "dataclass"
88
+ if is_dataclass(schema_type)
89
+ else "msgspec"
90
+ if is_msgspec_struct(schema_type)
91
+ else "pydantic"
92
+ if is_pydantic_model(schema_type)
93
+ else "attrs"
94
+ if is_attrs_schema(schema_type)
95
+ else None
96
+ )
97
+
98
+
99
+ def _convert_typed_dict(data: Any, schema_type: Any) -> Any:
100
+ """Convert data to TypedDict."""
101
+ return [item for item in data if is_dict(item)] if isinstance(data, list) else data
102
+
103
+
104
+ def _convert_dataclass(data: Any, schema_type: Any) -> Any:
105
+ """Convert data to dataclass."""
106
+ if isinstance(data, list):
107
+ return [schema_type(**dict(item)) if is_dict(item) else item for item in data]
108
+ return schema_type(**dict(data)) if is_dict(data) else (schema_type(**data) if isinstance(data, dict) else data)
109
+
110
+
111
+ _DEFAULT_TYPE_DECODERS: Final["list[tuple[Callable[[Any], bool], Callable[[Any, Any], Any]]]"] = [
112
+ (lambda x: x is UUID, lambda t, v: t(v.hex)),
113
+ (lambda x: x is datetime.datetime, lambda t, v: t(v.isoformat())),
114
+ (lambda x: x is datetime.date, lambda t, v: t(v.isoformat())),
115
+ (lambda x: x is datetime.time, lambda t, v: t(v.isoformat())),
116
+ (lambda x: x is Enum, lambda t, v: t(v.value)),
117
+ (_is_list_type_target, _convert_numpy_to_list),
118
+ ]
119
+
120
+
121
+ def _default_msgspec_deserializer(
122
+ target_type: Any, value: Any, type_decoders: "Sequence[tuple[Any, Any]] | None" = None
123
+ ) -> Any:
124
+ """Convert msgspec types with type decoder support.
125
+
126
+ Args:
127
+ target_type: Type to convert to
128
+ value: Value to convert
129
+ type_decoders: Optional sequence of (predicate, decoder) pairs
130
+
131
+ Returns:
132
+ Converted value or original value if conversion not applicable
133
+ """
134
+ if NUMPY_INSTALLED:
135
+ import numpy as np
136
+
137
+ if isinstance(value, np.ndarray) and _is_list_type_target(target_type):
138
+ return value.tolist()
139
+
140
+ if type_decoders:
141
+ for predicate, decoder in type_decoders:
142
+ if predicate(target_type):
143
+ return decoder(target_type, value)
144
+
145
+ if target_type is UUID and isinstance(value, UUID):
146
+ return value.hex
147
+
148
+ if target_type in _DATETIME_TYPES and hasattr(value, "isoformat"):
149
+ return value.isoformat() # pyright: ignore
150
+
151
+ if isinstance(target_type, type) and issubclass(target_type, Enum) and isinstance(value, Enum):
152
+ return value.value
153
+
154
+ try:
155
+ if isinstance(target_type, type) and isinstance(value, target_type):
156
+ return value
157
+ except TypeError:
158
+ pass
159
+
160
+ if isinstance(target_type, type):
161
+ try:
162
+ if issubclass(target_type, (Path, PurePath)) or issubclass(target_type, UUID):
163
+ return target_type(str(value))
164
+ except (TypeError, ValueError):
165
+ pass
166
+
167
+ return value
168
+
169
+
170
+ def _convert_msgspec(data: Any, schema_type: Any) -> Any:
171
+ """Convert data to msgspec Struct."""
172
+ rename_config = get_msgspec_rename_config(schema_type)
173
+ deserializer = partial(_default_msgspec_deserializer, type_decoders=_DEFAULT_TYPE_DECODERS)
174
+
175
+ transformed_data = data
176
+ if (rename_config and is_dict(data)) or (isinstance(data, Sequence) and data and is_dict(data[0])):
177
+ try:
178
+ converter_map: dict[str, Callable[[str], str]] = {"camel": camelize, "kebab": kebabize, "pascal": pascalize}
179
+ converter = converter_map.get(rename_config) if rename_config else None
180
+ if converter:
181
+ transformed_data = (
182
+ [transform_dict_keys(item, converter) if is_dict(item) else item for item in data]
183
+ if isinstance(data, Sequence)
184
+ else (transform_dict_keys(data, converter) if is_dict(data) else data)
185
+ )
186
+ except Exception as e:
187
+ logger.debug("Field name transformation failed for msgspec schema: %s", e)
188
+
189
+ if NUMPY_INSTALLED:
190
+ try:
191
+ import numpy as np
192
+
193
+ def _convert_numpy(obj: Any) -> Any:
194
+ return (
195
+ obj.tolist()
196
+ if isinstance(obj, np.ndarray)
197
+ else {k: _convert_numpy(v) for k, v in obj.items()}
198
+ if isinstance(obj, dict)
199
+ else type(obj)(_convert_numpy(item) for item in obj)
200
+ if isinstance(obj, (list, tuple))
201
+ else obj
202
+ )
203
+
204
+ transformed_data = _convert_numpy(transformed_data)
205
+ except ImportError:
206
+ pass
207
+
208
+ return convert(
209
+ obj=transformed_data,
210
+ type=(list[schema_type] if isinstance(transformed_data, Sequence) else schema_type),
211
+ from_attributes=True,
212
+ dec_hook=deserializer,
213
+ )
214
+
215
+
216
+ def _convert_pydantic(data: Any, schema_type: Any) -> Any:
217
+ """Convert data to Pydantic model."""
218
+ if isinstance(data, Sequence):
219
+ return get_type_adapter(list[schema_type]).validate_python(data, from_attributes=True)
220
+ return get_type_adapter(schema_type).validate_python(data, from_attributes=True)
221
+
222
+
223
+ def _convert_attrs(data: Any, schema_type: Any) -> Any:
224
+ """Convert data to attrs class."""
225
+ if CATTRS_INSTALLED:
226
+ if isinstance(data, Sequence):
227
+ return cattrs_structure(data, list[schema_type])
228
+ return cattrs_structure(cattrs_unstructure(data) if hasattr(data, "__attrs_attrs__") else data, schema_type)
229
+
230
+ if isinstance(data, list):
231
+ return [
232
+ schema_type(**dict(item)) if hasattr(item, "keys") else schema_type(**attrs_asdict(item)) for item in data
233
+ ]
234
+ return (
235
+ schema_type(**dict(data))
236
+ if hasattr(data, "keys")
237
+ else (schema_type(**data) if isinstance(data, dict) else data)
238
+ )
239
+
240
+
241
+ _SCHEMA_CONVERTERS: "dict[str, Callable[[Any, Any], Any]]" = {
242
+ "typed_dict": _convert_typed_dict,
243
+ "dataclass": _convert_dataclass,
244
+ "msgspec": _convert_msgspec,
245
+ "pydantic": _convert_pydantic,
246
+ "attrs": _convert_attrs,
247
+ }
248
+
249
+
250
+ @overload
251
+ def to_schema(data: "list[DataT]", *, schema_type: "type[SchemaT]") -> "list[SchemaT]": ...
252
+ @overload
253
+ def to_schema(data: "list[DataT]", *, schema_type: None = None) -> "list[DataT]": ...
254
+ @overload
255
+ def to_schema(data: "DataT", *, schema_type: "type[SchemaT]") -> "SchemaT": ...
256
+ @overload
257
+ def to_schema(data: "DataT", *, schema_type: None = None) -> "DataT": ...
258
+
259
+
260
+ def to_schema(data: Any, *, schema_type: Any = None) -> Any:
261
+ """Convert data to a specified schema type.
262
+
263
+ Supports transformation to various schema types including:
264
+ - TypedDict
265
+ - dataclasses
266
+ - msgspec Structs
267
+ - Pydantic models
268
+ - attrs classes
269
+
270
+ Args:
271
+ data: Input data to convert (dict, list of dicts, or other)
272
+ schema_type: Target schema type for conversion. If None, returns data unchanged.
273
+
274
+ Returns:
275
+ Converted data in the specified schema type, or original data if schema_type is None
276
+
277
+ Raises:
278
+ SQLSpecError: If schema_type is not a supported type
279
+ """
280
+ if schema_type is None:
281
+ return data
282
+
283
+ schema_type_key = _detect_schema_type(schema_type)
284
+ if schema_type_key is None:
285
+ msg = "`schema_type` should be a valid Dataclass, Pydantic model, Msgspec struct, Attrs class, or TypedDict"
286
+ raise SQLSpecError(msg)
287
+
288
+ return _SCHEMA_CONVERTERS[schema_type_key](data, schema_type)
@@ -4,7 +4,7 @@ Re-exports common JSON encoding and decoding functions from the core
4
4
  serialization module for convenient access.
5
5
  """
6
6
 
7
- from typing import Any, Literal, Union, overload
7
+ from typing import Any, Literal, overload
8
8
 
9
9
  from sqlspec._serialization import decode_json, encode_json
10
10
 
@@ -17,7 +17,7 @@ def to_json(data: Any, *, as_bytes: Literal[False] = ...) -> str: ...
17
17
  def to_json(data: Any, *, as_bytes: Literal[True]) -> bytes: ...
18
18
 
19
19
 
20
- def to_json(data: Any, *, as_bytes: bool = False) -> Union[str, bytes]:
20
+ def to_json(data: Any, *, as_bytes: bool = False) -> str | bytes:
21
21
  """Encode data to JSON string or bytes.
22
22
 
23
23
  Args:
@@ -40,7 +40,7 @@ def from_json(data: str) -> Any: ...
40
40
  def from_json(data: bytes, *, decode_bytes: bool = ...) -> Any: ...
41
41
 
42
42
 
43
- def from_json(data: Union[str, bytes], *, decode_bytes: bool = True) -> Any:
43
+ def from_json(data: str | bytes, *, decode_bytes: bool = True) -> Any:
44
44
  """Decode JSON string or bytes to Python object.
45
45
 
46
46
  Args:
@@ -8,9 +8,10 @@ for adapter implementations that need to support both sync and async patterns.
8
8
  import asyncio
9
9
  import functools
10
10
  import inspect
11
+ import os
11
12
  import sys
12
13
  from contextlib import AbstractAsyncContextManager, AbstractContextManager
13
- from typing import TYPE_CHECKING, Any, Generic, Optional, TypeVar, Union, cast
14
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast
14
15
 
15
16
  from typing_extensions import ParamSpec
16
17
 
@@ -46,13 +47,21 @@ class CapacityLimiter:
46
47
  total_tokens: Maximum number of concurrent operations allowed
47
48
  """
48
49
  self._total_tokens = total_tokens
49
- self._semaphore_instance: Optional[asyncio.Semaphore] = None
50
+ self._semaphore_instance: asyncio.Semaphore | None = None
51
+ self._pid: int | None = None
50
52
 
51
53
  @property
52
54
  def _semaphore(self) -> asyncio.Semaphore:
53
- """Lazy initialization of asyncio.Semaphore for Python 3.9 compatibility."""
54
- if self._semaphore_instance is None:
55
+ """Lazy initialization of asyncio.Semaphore with per-process tracking.
56
+
57
+ Reinitializes the semaphore if running in a new process (detected via PID).
58
+ This ensures pytest-xdist workers each get their own semaphore bound to
59
+ their event loop, preventing cross-process deadlocks.
60
+ """
61
+ current_pid = os.getpid()
62
+ if self._semaphore_instance is None or self._pid != current_pid:
55
63
  self._semaphore_instance = asyncio.Semaphore(self._total_tokens)
64
+ self._pid = current_pid
56
65
  return self._semaphore_instance
57
66
 
58
67
  async def acquire(self) -> None:
@@ -72,22 +81,20 @@ class CapacityLimiter:
72
81
  def total_tokens(self, value: int) -> None:
73
82
  self._total_tokens = value
74
83
  self._semaphore_instance = None
84
+ self._pid = None
75
85
 
76
86
  async def __aenter__(self) -> None:
77
87
  """Async context manager entry."""
78
88
  await self.acquire()
79
89
 
80
90
  async def __aexit__(
81
- self,
82
- exc_type: "Optional[type[BaseException]]",
83
- exc_val: "Optional[BaseException]",
84
- exc_tb: "Optional[TracebackType]",
91
+ self, exc_type: "type[BaseException] | None", exc_val: "BaseException | None", exc_tb: "TracebackType | None"
85
92
  ) -> None:
86
93
  """Async context manager exit."""
87
94
  self.release()
88
95
 
89
96
 
90
- _default_limiter = CapacityLimiter(15)
97
+ _default_limiter = CapacityLimiter(1000)
91
98
 
92
99
 
93
100
  def run_(async_function: "Callable[ParamSpecT, Coroutine[Any, Any, ReturnT]]") -> "Callable[ParamSpecT, ReturnT]":
@@ -169,7 +176,7 @@ def await_(
169
176
 
170
177
 
171
178
  def async_(
172
- function: "Callable[ParamSpecT, ReturnT]", *, limiter: "Optional[CapacityLimiter]" = None
179
+ function: "Callable[ParamSpecT, ReturnT]", *, limiter: "CapacityLimiter | None" = None
173
180
  ) -> "Callable[ParamSpecT, Awaitable[ReturnT]]":
174
181
  """Convert a blocking function to an async one using asyncio.to_thread().
175
182
 
@@ -192,7 +199,7 @@ def async_(
192
199
 
193
200
 
194
201
  def ensure_async_(
195
- function: "Callable[ParamSpecT, Union[Awaitable[ReturnT], ReturnT]]",
202
+ function: "Callable[ParamSpecT, Awaitable[ReturnT] | ReturnT]",
196
203
  ) -> "Callable[ParamSpecT, Awaitable[ReturnT]]":
197
204
  """Convert a function to an async one if it is not already.
198
205
 
@@ -223,16 +230,13 @@ class _ContextManagerWrapper(Generic[T]):
223
230
  return self._cm.__enter__()
224
231
 
225
232
  async def __aexit__(
226
- self,
227
- exc_type: "Optional[type[BaseException]]",
228
- exc_val: "Optional[BaseException]",
229
- exc_tb: "Optional[TracebackType]",
230
- ) -> "Optional[bool]":
233
+ self, exc_type: "type[BaseException] | None", exc_val: "BaseException | None", exc_tb: "TracebackType | None"
234
+ ) -> "bool | None":
231
235
  return self._cm.__exit__(exc_type, exc_val, exc_tb)
232
236
 
233
237
 
234
238
  def with_ensure_async_(
235
- obj: "Union[AbstractContextManager[T], AbstractAsyncContextManager[T]]",
239
+ obj: "AbstractContextManager[T] | AbstractAsyncContextManager[T]",
236
240
  ) -> "AbstractAsyncContextManager[T]":
237
241
  """Convert a context manager to an async one if it is not already.
238
242
 
sqlspec/utils/text.py CHANGED
@@ -8,7 +8,6 @@ generation and data validation.
8
8
  import re
9
9
  import unicodedata
10
10
  from functools import lru_cache
11
- from typing import Optional
12
11
 
13
12
  _SLUGIFY_REMOVE_NON_ALPHANUMERIC = re.compile(r"[^\w]+", re.UNICODE)
14
13
  _SLUGIFY_HYPHEN_COLLAPSE = re.compile(r"-+")
@@ -22,7 +21,7 @@ _SNAKE_CASE_MULTIPLE_UNDERSCORES = re.compile(r"__+", re.UNICODE)
22
21
  __all__ = ("camelize", "kebabize", "pascalize", "slugify", "snake_case")
23
22
 
24
23
 
25
- def slugify(value: str, allow_unicode: bool = False, separator: Optional[str] = None) -> str:
24
+ def slugify(value: str, allow_unicode: bool = False, separator: str | None = None) -> str:
26
25
  """Convert a string to a URL-friendly slug.
27
26
 
28
27
  Args: