sqlspec 0.26.0__py3-none-any.whl → 0.28.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 (212) hide show
  1. sqlspec/__init__.py +7 -15
  2. sqlspec/_serialization.py +55 -25
  3. sqlspec/_typing.py +155 -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 +880 -0
  7. sqlspec/adapters/adbc/config.py +62 -12
  8. sqlspec/adapters/adbc/data_dictionary.py +74 -2
  9. sqlspec/adapters/adbc/driver.py +226 -58
  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 +536 -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 +503 -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 +460 -0
  36. sqlspec/adapters/asyncpg/config.py +57 -36
  37. sqlspec/adapters/asyncpg/data_dictionary.py +48 -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 +585 -0
  44. sqlspec/adapters/bigquery/config.py +36 -11
  45. sqlspec/adapters/bigquery/data_dictionary.py +42 -2
  46. sqlspec/adapters/bigquery/driver.py +489 -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 +563 -0
  53. sqlspec/adapters/duckdb/config.py +79 -21
  54. sqlspec/adapters/duckdb/data_dictionary.py +41 -2
  55. sqlspec/adapters/duckdb/driver.py +225 -44
  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 +1628 -0
  64. sqlspec/adapters/oracledb/config.py +120 -36
  65. sqlspec/adapters/oracledb/data_dictionary.py +87 -20
  66. sqlspec/adapters/oracledb/driver.py +475 -86
  67. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  68. sqlspec/adapters/oracledb/litestar/store.py +765 -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 +483 -0
  75. sqlspec/adapters/psqlpy/config.py +45 -19
  76. sqlspec/adapters/psqlpy/data_dictionary.py +48 -2
  77. sqlspec/adapters/psqlpy/driver.py +108 -41
  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 +962 -0
  85. sqlspec/adapters/psycopg/config.py +65 -37
  86. sqlspec/adapters/psycopg/data_dictionary.py +91 -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 +582 -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 +331 -62
  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 +55 -47
  121. sqlspec/core/hashing.py +9 -9
  122. sqlspec/core/parameters.py +76 -45
  123. sqlspec/core/result.py +234 -47
  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 +183 -160
  129. sqlspec/driver/_common.py +197 -109
  130. sqlspec/driver/_sync.py +189 -161
  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 +69 -61
  142. sqlspec/extensions/fastapi/__init__.py +21 -0
  143. sqlspec/extensions/fastapi/extension.py +331 -0
  144. sqlspec/extensions/fastapi/providers.py +543 -0
  145. sqlspec/extensions/flask/__init__.py +36 -0
  146. sqlspec/extensions/flask/_state.py +71 -0
  147. sqlspec/extensions/flask/_utils.py +40 -0
  148. sqlspec/extensions/flask/extension.py +389 -0
  149. sqlspec/extensions/litestar/__init__.py +21 -4
  150. sqlspec/extensions/litestar/cli.py +54 -10
  151. sqlspec/extensions/litestar/config.py +56 -266
  152. sqlspec/extensions/litestar/handlers.py +46 -17
  153. sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
  154. sqlspec/extensions/litestar/migrations/__init__.py +3 -0
  155. sqlspec/extensions/litestar/plugin.py +349 -224
  156. sqlspec/extensions/litestar/providers.py +25 -25
  157. sqlspec/extensions/litestar/store.py +265 -0
  158. sqlspec/extensions/starlette/__init__.py +10 -0
  159. sqlspec/extensions/starlette/_state.py +25 -0
  160. sqlspec/extensions/starlette/_utils.py +52 -0
  161. sqlspec/extensions/starlette/extension.py +254 -0
  162. sqlspec/extensions/starlette/middleware.py +154 -0
  163. sqlspec/loader.py +30 -49
  164. sqlspec/migrations/base.py +200 -76
  165. sqlspec/migrations/commands.py +591 -62
  166. sqlspec/migrations/context.py +6 -9
  167. sqlspec/migrations/fix.py +199 -0
  168. sqlspec/migrations/loaders.py +47 -19
  169. sqlspec/migrations/runner.py +241 -75
  170. sqlspec/migrations/tracker.py +237 -21
  171. sqlspec/migrations/utils.py +51 -3
  172. sqlspec/migrations/validation.py +177 -0
  173. sqlspec/protocols.py +106 -36
  174. sqlspec/storage/_utils.py +85 -0
  175. sqlspec/storage/backends/fsspec.py +133 -107
  176. sqlspec/storage/backends/local.py +78 -51
  177. sqlspec/storage/backends/obstore.py +276 -168
  178. sqlspec/storage/registry.py +75 -39
  179. sqlspec/typing.py +30 -84
  180. sqlspec/utils/__init__.py +25 -4
  181. sqlspec/utils/arrow_helpers.py +81 -0
  182. sqlspec/utils/config_resolver.py +6 -6
  183. sqlspec/utils/correlation.py +4 -5
  184. sqlspec/utils/data_transformation.py +3 -2
  185. sqlspec/utils/deprecation.py +9 -8
  186. sqlspec/utils/fixtures.py +4 -4
  187. sqlspec/utils/logging.py +46 -6
  188. sqlspec/utils/module_loader.py +205 -5
  189. sqlspec/utils/portal.py +311 -0
  190. sqlspec/utils/schema.py +288 -0
  191. sqlspec/utils/serializers.py +113 -4
  192. sqlspec/utils/sync_tools.py +36 -22
  193. sqlspec/utils/text.py +1 -2
  194. sqlspec/utils/type_guards.py +136 -20
  195. sqlspec/utils/version.py +433 -0
  196. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/METADATA +41 -22
  197. sqlspec-0.28.0.dist-info/RECORD +221 -0
  198. sqlspec/builder/mixins/__init__.py +0 -55
  199. sqlspec/builder/mixins/_cte_and_set_ops.py +0 -253
  200. sqlspec/builder/mixins/_delete_operations.py +0 -50
  201. sqlspec/builder/mixins/_insert_operations.py +0 -282
  202. sqlspec/builder/mixins/_merge_operations.py +0 -698
  203. sqlspec/builder/mixins/_order_limit_operations.py +0 -145
  204. sqlspec/builder/mixins/_pivot_operations.py +0 -157
  205. sqlspec/builder/mixins/_select_operations.py +0 -930
  206. sqlspec/builder/mixins/_update_operations.py +0 -199
  207. sqlspec/builder/mixins/_where_clause.py +0 -1298
  208. sqlspec-0.26.0.dist-info/RECORD +0 -157
  209. sqlspec-0.26.0.dist-info/licenses/NOTICE +0 -29
  210. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/WHEEL +0 -0
  211. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/entry_points.txt +0 -0
  212. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/licenses/LICENSE +0 -0
@@ -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)
@@ -2,11 +2,15 @@
2
2
 
3
3
  Re-exports common JSON encoding and decoding functions from the core
4
4
  serialization module for convenient access.
5
+
6
+ Provides NumPy array serialization hooks for framework integrations
7
+ that support custom type encoders and decoders (e.g., Litestar).
5
8
  """
6
9
 
7
- from typing import Any, Literal, Union, overload
10
+ from typing import Any, Literal, overload
8
11
 
9
12
  from sqlspec._serialization import decode_json, encode_json
13
+ from sqlspec.typing import NUMPY_INSTALLED
10
14
 
11
15
 
12
16
  @overload
@@ -17,7 +21,7 @@ def to_json(data: Any, *, as_bytes: Literal[False] = ...) -> str: ...
17
21
  def to_json(data: Any, *, as_bytes: Literal[True]) -> bytes: ...
18
22
 
19
23
 
20
- def to_json(data: Any, *, as_bytes: bool = False) -> Union[str, bytes]:
24
+ def to_json(data: Any, *, as_bytes: bool = False) -> str | bytes:
21
25
  """Encode data to JSON string or bytes.
22
26
 
23
27
  Args:
@@ -40,7 +44,7 @@ def from_json(data: str) -> Any: ...
40
44
  def from_json(data: bytes, *, decode_bytes: bool = ...) -> Any: ...
41
45
 
42
46
 
43
- def from_json(data: Union[str, bytes], *, decode_bytes: bool = True) -> Any:
47
+ def from_json(data: str | bytes, *, decode_bytes: bool = True) -> Any:
44
48
  """Decode JSON string or bytes to Python object.
45
49
 
46
50
  Args:
@@ -55,4 +59,109 @@ def from_json(data: Union[str, bytes], *, decode_bytes: bool = True) -> Any:
55
59
  return decode_json(data)
56
60
 
57
61
 
58
- __all__ = ("from_json", "to_json")
62
+ def numpy_array_enc_hook(value: Any) -> Any:
63
+ """Encode NumPy array to JSON-compatible list.
64
+
65
+ Converts NumPy ndarrays to Python lists for JSON serialization.
66
+ Gracefully handles cases where NumPy is not installed by returning
67
+ the original value unchanged.
68
+
69
+ Args:
70
+ value: Value to encode (checked for ndarray type).
71
+
72
+ Returns:
73
+ List representation if value is ndarray, original value otherwise.
74
+
75
+ Example:
76
+ >>> import numpy as np
77
+ >>> arr = np.array([1.0, 2.0, 3.0])
78
+ >>> numpy_array_enc_hook(arr)
79
+ [1.0, 2.0, 3.0]
80
+
81
+ >>> # Multi-dimensional arrays work automatically
82
+ >>> arr_2d = np.array([[1, 2], [3, 4]])
83
+ >>> numpy_array_enc_hook(arr_2d)
84
+ [[1, 2], [3, 4]]
85
+ """
86
+ if not NUMPY_INSTALLED:
87
+ return value
88
+
89
+ import numpy as np
90
+
91
+ if isinstance(value, np.ndarray):
92
+ return value.tolist()
93
+ return value
94
+
95
+
96
+ def numpy_array_dec_hook(value: Any) -> "Any":
97
+ """Decode list to NumPy array.
98
+
99
+ Converts Python lists to NumPy arrays when appropriate.
100
+ Works best with typed schemas (Pydantic, msgspec) that expect ndarray.
101
+
102
+ Args:
103
+ value: List to potentially convert to ndarray.
104
+
105
+ Returns:
106
+ NumPy array if conversion successful, original value otherwise.
107
+
108
+ Note:
109
+ Dtype is inferred by NumPy and may differ from original array.
110
+ For explicit dtype control, construct arrays manually in application code.
111
+
112
+ Example:
113
+ >>> numpy_array_dec_hook([1.0, 2.0, 3.0])
114
+ array([1., 2., 3.])
115
+
116
+ >>> # Returns original value if NumPy not installed
117
+ >>> # (when NUMPY_INSTALLED is False)
118
+ >>> numpy_array_dec_hook([1, 2, 3])
119
+ [1, 2, 3]
120
+ """
121
+ if not NUMPY_INSTALLED:
122
+ return value
123
+
124
+ import numpy as np
125
+
126
+ if isinstance(value, list):
127
+ try:
128
+ return np.array(value)
129
+ except Exception:
130
+ return value
131
+ return value
132
+
133
+
134
+ def numpy_array_predicate(value: Any) -> bool:
135
+ """Check if value is NumPy array instance.
136
+
137
+ Type checker for decoder registration in framework plugins.
138
+ Returns False when NumPy is not installed.
139
+
140
+ Args:
141
+ value: Value to type-check.
142
+
143
+ Returns:
144
+ True if value is ndarray, False otherwise.
145
+
146
+ Example:
147
+ >>> import numpy as np
148
+ >>> numpy_array_predicate(np.array([1, 2, 3]))
149
+ True
150
+
151
+ >>> numpy_array_predicate([1, 2, 3])
152
+ False
153
+
154
+ >>> # Returns False when NumPy not installed
155
+ >>> # (when NUMPY_INSTALLED is False)
156
+ >>> numpy_array_predicate([1, 2, 3])
157
+ False
158
+ """
159
+ if not NUMPY_INSTALLED:
160
+ return False
161
+
162
+ import numpy as np
163
+
164
+ return isinstance(value, np.ndarray)
165
+
166
+
167
+ __all__ = ("from_json", "numpy_array_dec_hook", "numpy_array_enc_hook", "numpy_array_predicate", "to_json")
@@ -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]":
@@ -125,14 +132,18 @@ def run_(async_function: "Callable[ParamSpecT, Coroutine[Any, Any, ReturnT]]") -
125
132
 
126
133
 
127
134
  def await_(
128
- async_function: "Callable[ParamSpecT, Coroutine[Any, Any, ReturnT]]", raise_sync_error: bool = True
135
+ async_function: "Callable[ParamSpecT, Coroutine[Any, Any, ReturnT]]", raise_sync_error: bool = False
129
136
  ) -> "Callable[ParamSpecT, ReturnT]":
130
137
  """Convert an async function to a blocking one, running in the main async loop.
131
138
 
139
+ When no event loop exists, automatically creates and uses a global portal for
140
+ async-to-sync bridging via background thread. Set raise_sync_error=True to
141
+ disable this behavior and raise errors instead.
142
+
132
143
  Args:
133
144
  async_function: The async function to convert.
134
- raise_sync_error: If False, runs in a new event loop if no loop is present.
135
- If True (default), raises RuntimeError if no loop is running.
145
+ raise_sync_error: If True, raises RuntimeError when no loop exists.
146
+ If False (default), uses portal pattern for automatic bridging.
136
147
 
137
148
  Returns:
138
149
  A blocking function that runs the async function.
@@ -147,7 +158,10 @@ def await_(
147
158
  if raise_sync_error:
148
159
  msg = "Cannot run async function"
149
160
  raise RuntimeError(msg) from None
150
- return asyncio.run(partial_f())
161
+ from sqlspec.utils.portal import get_global_portal
162
+
163
+ portal = get_global_portal()
164
+ return portal.call(async_function, *args, **kwargs)
151
165
  else:
152
166
  if loop.is_running():
153
167
  try:
@@ -163,13 +177,16 @@ def await_(
163
177
  if raise_sync_error:
164
178
  msg = "Cannot run async function"
165
179
  raise RuntimeError(msg)
166
- return asyncio.run(partial_f())
180
+ from sqlspec.utils.portal import get_global_portal
181
+
182
+ portal = get_global_portal()
183
+ return portal.call(async_function, *args, **kwargs)
167
184
 
168
185
  return wrapper
169
186
 
170
187
 
171
188
  def async_(
172
- function: "Callable[ParamSpecT, ReturnT]", *, limiter: "Optional[CapacityLimiter]" = None
189
+ function: "Callable[ParamSpecT, ReturnT]", *, limiter: "CapacityLimiter | None" = None
173
190
  ) -> "Callable[ParamSpecT, Awaitable[ReturnT]]":
174
191
  """Convert a blocking function to an async one using asyncio.to_thread().
175
192
 
@@ -192,7 +209,7 @@ def async_(
192
209
 
193
210
 
194
211
  def ensure_async_(
195
- function: "Callable[ParamSpecT, Union[Awaitable[ReturnT], ReturnT]]",
212
+ function: "Callable[ParamSpecT, Awaitable[ReturnT] | ReturnT]",
196
213
  ) -> "Callable[ParamSpecT, Awaitable[ReturnT]]":
197
214
  """Convert a function to an async one if it is not already.
198
215
 
@@ -223,16 +240,13 @@ class _ContextManagerWrapper(Generic[T]):
223
240
  return self._cm.__enter__()
224
241
 
225
242
  async def __aexit__(
226
- self,
227
- exc_type: "Optional[type[BaseException]]",
228
- exc_val: "Optional[BaseException]",
229
- exc_tb: "Optional[TracebackType]",
230
- ) -> "Optional[bool]":
243
+ self, exc_type: "type[BaseException] | None", exc_val: "BaseException | None", exc_tb: "TracebackType | None"
244
+ ) -> "bool | None":
231
245
  return self._cm.__exit__(exc_type, exc_val, exc_tb)
232
246
 
233
247
 
234
248
  def with_ensure_async_(
235
- obj: "Union[AbstractContextManager[T], AbstractAsyncContextManager[T]]",
249
+ obj: "AbstractContextManager[T] | AbstractAsyncContextManager[T]",
236
250
  ) -> "AbstractAsyncContextManager[T]":
237
251
  """Convert a context manager to an async one if it is not already.
238
252
 
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: