sqlspec 0.25.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 (199) hide show
  1. sqlspec/__init__.py +7 -15
  2. sqlspec/_serialization.py +256 -24
  3. sqlspec/_typing.py +71 -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 +69 -12
  8. sqlspec/adapters/adbc/data_dictionary.py +340 -0
  9. sqlspec/adapters/adbc/driver.py +266 -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 +153 -0
  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 +88 -15
  17. sqlspec/adapters/aiosqlite/data_dictionary.py +149 -0
  18. sqlspec/adapters/aiosqlite/driver.py +143 -40
  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 +2 -2
  24. sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
  25. sqlspec/adapters/asyncmy/adk/store.py +493 -0
  26. sqlspec/adapters/asyncmy/config.py +68 -23
  27. sqlspec/adapters/asyncmy/data_dictionary.py +161 -0
  28. sqlspec/adapters/asyncmy/driver.py +313 -58
  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 +59 -35
  37. sqlspec/adapters/asyncpg/data_dictionary.py +173 -0
  38. sqlspec/adapters/asyncpg/driver.py +170 -25
  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 +27 -10
  45. sqlspec/adapters/bigquery/data_dictionary.py +149 -0
  46. sqlspec/adapters/bigquery/driver.py +368 -142
  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 +125 -0
  50. sqlspec/adapters/duckdb/_types.py +1 -1
  51. sqlspec/adapters/duckdb/adk/__init__.py +14 -0
  52. sqlspec/adapters/duckdb/adk/store.py +553 -0
  53. sqlspec/adapters/duckdb/config.py +80 -20
  54. sqlspec/adapters/duckdb/data_dictionary.py +163 -0
  55. sqlspec/adapters/duckdb/driver.py +167 -45
  56. sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
  57. sqlspec/adapters/duckdb/litestar/store.py +332 -0
  58. sqlspec/adapters/duckdb/pool.py +4 -4
  59. sqlspec/adapters/duckdb/type_converter.py +133 -0
  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 +122 -32
  65. sqlspec/adapters/oracledb/data_dictionary.py +509 -0
  66. sqlspec/adapters/oracledb/driver.py +353 -91
  67. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  68. sqlspec/adapters/oracledb/litestar/store.py +767 -0
  69. sqlspec/adapters/oracledb/migrations.py +348 -73
  70. sqlspec/adapters/oracledb/type_converter.py +207 -0
  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 +46 -17
  76. sqlspec/adapters/psqlpy/data_dictionary.py +172 -0
  77. sqlspec/adapters/psqlpy/driver.py +123 -209
  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 +102 -0
  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 +69 -35
  86. sqlspec/adapters/psycopg/data_dictionary.py +331 -0
  87. sqlspec/adapters/psycopg/driver.py +238 -81
  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 +87 -15
  96. sqlspec/adapters/sqlite/data_dictionary.py +149 -0
  97. sqlspec/adapters/sqlite/driver.py +137 -54
  98. sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
  99. sqlspec/adapters/sqlite/litestar/store.py +318 -0
  100. sqlspec/adapters/sqlite/pool.py +18 -9
  101. sqlspec/base.py +45 -26
  102. sqlspec/builder/__init__.py +73 -4
  103. sqlspec/builder/_base.py +162 -89
  104. sqlspec/builder/_column.py +62 -29
  105. sqlspec/builder/_ddl.py +180 -121
  106. sqlspec/builder/_delete.py +5 -4
  107. sqlspec/builder/_dml.py +388 -0
  108. sqlspec/{_sql.py → builder/_factory.py} +53 -94
  109. sqlspec/builder/_insert.py +32 -131
  110. sqlspec/builder/_join.py +375 -0
  111. sqlspec/builder/_merge.py +446 -11
  112. sqlspec/builder/_parsing_utils.py +111 -17
  113. sqlspec/builder/_select.py +1457 -24
  114. sqlspec/builder/_update.py +11 -42
  115. sqlspec/cli.py +307 -194
  116. sqlspec/config.py +252 -67
  117. sqlspec/core/__init__.py +5 -4
  118. sqlspec/core/cache.py +17 -17
  119. sqlspec/core/compiler.py +62 -9
  120. sqlspec/core/filters.py +37 -37
  121. sqlspec/core/hashing.py +9 -9
  122. sqlspec/core/parameters.py +83 -48
  123. sqlspec/core/result.py +102 -46
  124. sqlspec/core/splitter.py +16 -17
  125. sqlspec/core/statement.py +36 -30
  126. sqlspec/core/type_conversion.py +235 -0
  127. sqlspec/driver/__init__.py +7 -6
  128. sqlspec/driver/_async.py +188 -151
  129. sqlspec/driver/_common.py +285 -80
  130. sqlspec/driver/_sync.py +188 -152
  131. sqlspec/driver/mixins/_result_tools.py +20 -236
  132. sqlspec/driver/mixins/_sql_translator.py +4 -4
  133. sqlspec/exceptions.py +75 -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/__init__.py +4 -3
  153. sqlspec/migrations/base.py +302 -39
  154. sqlspec/migrations/commands.py +611 -144
  155. sqlspec/migrations/context.py +142 -0
  156. sqlspec/migrations/fix.py +199 -0
  157. sqlspec/migrations/loaders.py +68 -23
  158. sqlspec/migrations/runner.py +543 -107
  159. sqlspec/migrations/tracker.py +237 -21
  160. sqlspec/migrations/utils.py +51 -3
  161. sqlspec/migrations/validation.py +177 -0
  162. sqlspec/protocols.py +66 -36
  163. sqlspec/storage/_utils.py +98 -0
  164. sqlspec/storage/backends/fsspec.py +134 -106
  165. sqlspec/storage/backends/local.py +78 -51
  166. sqlspec/storage/backends/obstore.py +278 -162
  167. sqlspec/storage/registry.py +75 -39
  168. sqlspec/typing.py +16 -84
  169. sqlspec/utils/config_resolver.py +153 -0
  170. sqlspec/utils/correlation.py +4 -5
  171. sqlspec/utils/data_transformation.py +3 -2
  172. sqlspec/utils/deprecation.py +9 -8
  173. sqlspec/utils/fixtures.py +4 -4
  174. sqlspec/utils/logging.py +46 -6
  175. sqlspec/utils/module_loader.py +2 -2
  176. sqlspec/utils/schema.py +288 -0
  177. sqlspec/utils/serializers.py +50 -2
  178. sqlspec/utils/sync_tools.py +21 -17
  179. sqlspec/utils/text.py +1 -2
  180. sqlspec/utils/type_guards.py +111 -20
  181. sqlspec/utils/version.py +433 -0
  182. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/METADATA +40 -21
  183. sqlspec-0.27.0.dist-info/RECORD +207 -0
  184. sqlspec/builder/mixins/__init__.py +0 -55
  185. sqlspec/builder/mixins/_cte_and_set_ops.py +0 -254
  186. sqlspec/builder/mixins/_delete_operations.py +0 -50
  187. sqlspec/builder/mixins/_insert_operations.py +0 -282
  188. sqlspec/builder/mixins/_join_operations.py +0 -389
  189. sqlspec/builder/mixins/_merge_operations.py +0 -592
  190. sqlspec/builder/mixins/_order_limit_operations.py +0 -152
  191. sqlspec/builder/mixins/_pivot_operations.py +0 -157
  192. sqlspec/builder/mixins/_select_operations.py +0 -936
  193. sqlspec/builder/mixins/_update_operations.py +0 -218
  194. sqlspec/builder/mixins/_where_clause.py +0 -1304
  195. sqlspec-0.25.0.dist-info/RECORD +0 -139
  196. sqlspec-0.25.0.dist-info/licenses/NOTICE +0 -29
  197. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/WHEEL +0 -0
  198. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/entry_points.txt +0 -0
  199. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/licenses/LICENSE +0 -0
sqlspec/utils/fixtures.py CHANGED
@@ -7,7 +7,7 @@ used in testing and development. Supports both sync and async operations.
7
7
  import gzip
8
8
  import zipfile
9
9
  from pathlib import Path
10
- from typing import TYPE_CHECKING, Any, Union
10
+ from typing import TYPE_CHECKING, Any
11
11
 
12
12
  from sqlspec.storage import storage_registry
13
13
  from sqlspec.utils.serializers import from_json as decode_json
@@ -16,7 +16,7 @@ from sqlspec.utils.sync_tools import async_
16
16
  from sqlspec.utils.type_guards import schema_dump
17
17
 
18
18
  if TYPE_CHECKING:
19
- from sqlspec.typing import ModelDictList, SupportedSchemaModel
19
+ from sqlspec.typing import SupportedSchemaModel
20
20
 
21
21
  __all__ = ("open_fixture", "open_fixture_async", "write_fixture", "write_fixture_async")
22
22
 
@@ -171,7 +171,7 @@ def _serialize_data(data: Any) -> str:
171
171
  def write_fixture(
172
172
  fixtures_path: str,
173
173
  table_name: str,
174
- data: "Union[ModelDictList, list[dict[str, Any]], SupportedSchemaModel]",
174
+ data: "list[SupportedSchemaModel] | list[dict[str, Any]] | SupportedSchemaModel",
175
175
  storage_backend: str = "local",
176
176
  compress: bool = False,
177
177
  **storage_kwargs: Any,
@@ -219,7 +219,7 @@ def write_fixture(
219
219
  async def write_fixture_async(
220
220
  fixtures_path: str,
221
221
  table_name: str,
222
- data: "Union[ModelDictList, list[dict[str, Any]], SupportedSchemaModel]",
222
+ data: "list[SupportedSchemaModel] | list[dict[str, Any]] | SupportedSchemaModel",
223
223
  storage_backend: str = "local",
224
224
  compress: bool = False,
225
225
  **storage_kwargs: Any,
sqlspec/utils/logging.py CHANGED
@@ -8,16 +8,24 @@ SQLSpec provides StructuredFormatter for JSON-formatted logs if desired.
8
8
  import logging
9
9
  from contextvars import ContextVar
10
10
  from logging import LogRecord
11
- from typing import Any, Optional
11
+ from typing import Any
12
12
 
13
13
  from sqlspec._serialization import encode_json
14
14
 
15
- __all__ = ("StructuredFormatter", "correlation_id_var", "get_correlation_id", "get_logger", "set_correlation_id")
15
+ __all__ = (
16
+ "SqlglotCommandFallbackFilter",
17
+ "StructuredFormatter",
18
+ "correlation_id_var",
19
+ "get_correlation_id",
20
+ "get_logger",
21
+ "set_correlation_id",
22
+ "suppress_erroneous_sqlglot_log_messages",
23
+ )
16
24
 
17
- correlation_id_var: "ContextVar[Optional[str]]" = ContextVar("correlation_id", default=None)
25
+ correlation_id_var: "ContextVar[str | None]" = ContextVar("correlation_id", default=None)
18
26
 
19
27
 
20
- def set_correlation_id(correlation_id: "Optional[str]") -> None:
28
+ def set_correlation_id(correlation_id: "str | None") -> None:
21
29
  """Set the correlation ID for the current context.
22
30
 
23
31
  Args:
@@ -26,7 +34,7 @@ def set_correlation_id(correlation_id: "Optional[str]") -> None:
26
34
  correlation_id_var.set(correlation_id)
27
35
 
28
36
 
29
- def get_correlation_id() -> "Optional[str]":
37
+ def get_correlation_id() -> "str | None":
30
38
  """Get the current correlation ID.
31
39
 
32
40
  Returns:
@@ -86,7 +94,27 @@ class CorrelationIDFilter(logging.Filter):
86
94
  return True
87
95
 
88
96
 
89
- def get_logger(name: "Optional[str]" = None) -> logging.Logger:
97
+ class SqlglotCommandFallbackFilter(logging.Filter):
98
+ """Filter to suppress sqlglot's confusing 'Falling back to Command' warning.
99
+
100
+ This filter suppresses the warning message that sqlglot emits when it
101
+ encounters unsupported syntax and falls back to parsing as a Command.
102
+ This is expected behavior in SQLSpec and the warning is confusing to users.
103
+ """
104
+
105
+ def filter(self, record: LogRecord) -> bool:
106
+ """Suppress the 'Falling back to Command' warning message.
107
+
108
+ Args:
109
+ record: The log record to evaluate
110
+
111
+ Returns:
112
+ False if the record contains the fallback warning, True otherwise
113
+ """
114
+ return "Falling back to parsing as a 'Command'" not in record.getMessage()
115
+
116
+
117
+ def get_logger(name: "str | None" = None) -> logging.Logger:
90
118
  """Get a logger instance with standardized configuration.
91
119
 
92
120
  Args:
@@ -121,3 +149,15 @@ def log_with_context(logger: logging.Logger, level: int, message: str, **extra_f
121
149
  record = logger.makeRecord(logger.name, level, "(unknown file)", 0, message, (), None)
122
150
  record.extra_fields = extra_fields
123
151
  logger.handle(record)
152
+
153
+
154
+ def suppress_erroneous_sqlglot_log_messages() -> None:
155
+ """Suppress confusing sqlglot warning messages.
156
+
157
+ Adds a filter to the sqlglot logger to suppress the warning message
158
+ about falling back to parsing as a Command. This is expected behavior
159
+ in SQLSpec and the warning is confusing to users.
160
+ """
161
+ sqlglot_logger = logging.getLogger("sqlglot")
162
+ if not any(isinstance(f, SqlglotCommandFallbackFilter) for f in sqlglot_logger.filters):
163
+ sqlglot_logger.addFilter(SqlglotCommandFallbackFilter())
@@ -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,55 @@ Re-exports common JSON encoding and decoding functions from the core
4
4
  serialization module for convenient access.
5
5
  """
6
6
 
7
- from sqlspec._serialization import decode_json as from_json
8
- from sqlspec._serialization import encode_json as to_json
7
+ from typing import Any, Literal, overload
8
+
9
+ from sqlspec._serialization import decode_json, encode_json
10
+
11
+
12
+ @overload
13
+ def to_json(data: Any, *, as_bytes: Literal[False] = ...) -> str: ...
14
+
15
+
16
+ @overload
17
+ def to_json(data: Any, *, as_bytes: Literal[True]) -> bytes: ...
18
+
19
+
20
+ def to_json(data: Any, *, as_bytes: bool = False) -> str | bytes:
21
+ """Encode data to JSON string or bytes.
22
+
23
+ Args:
24
+ data: Data to encode.
25
+ as_bytes: Whether to return bytes instead of string for optimal performance.
26
+
27
+ Returns:
28
+ JSON string or bytes representation based on as_bytes parameter.
29
+ """
30
+ if as_bytes:
31
+ return encode_json(data, as_bytes=True)
32
+ return encode_json(data, as_bytes=False)
33
+
34
+
35
+ @overload
36
+ def from_json(data: str) -> Any: ...
37
+
38
+
39
+ @overload
40
+ def from_json(data: bytes, *, decode_bytes: bool = ...) -> Any: ...
41
+
42
+
43
+ def from_json(data: str | bytes, *, decode_bytes: bool = True) -> Any:
44
+ """Decode JSON string or bytes to Python object.
45
+
46
+ Args:
47
+ data: JSON string or bytes to decode.
48
+ decode_bytes: Whether to decode bytes input (vs passing through).
49
+
50
+ Returns:
51
+ Decoded Python object.
52
+ """
53
+ if isinstance(data, bytes):
54
+ return decode_json(data, decode_bytes=decode_bytes)
55
+ return decode_json(data)
56
+
9
57
 
10
58
  __all__ = ("from_json", "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]":
@@ -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: