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.
- sqlspec/__init__.py +7 -15
- sqlspec/_serialization.py +55 -25
- sqlspec/_typing.py +155 -52
- sqlspec/adapters/adbc/_types.py +1 -1
- sqlspec/adapters/adbc/adk/__init__.py +5 -0
- sqlspec/adapters/adbc/adk/store.py +880 -0
- sqlspec/adapters/adbc/config.py +62 -12
- sqlspec/adapters/adbc/data_dictionary.py +74 -2
- sqlspec/adapters/adbc/driver.py +226 -58
- sqlspec/adapters/adbc/litestar/__init__.py +5 -0
- sqlspec/adapters/adbc/litestar/store.py +504 -0
- sqlspec/adapters/adbc/type_converter.py +44 -50
- sqlspec/adapters/aiosqlite/_types.py +1 -1
- sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/adk/store.py +536 -0
- sqlspec/adapters/aiosqlite/config.py +86 -16
- sqlspec/adapters/aiosqlite/data_dictionary.py +34 -2
- sqlspec/adapters/aiosqlite/driver.py +127 -38
- sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/litestar/store.py +281 -0
- sqlspec/adapters/aiosqlite/pool.py +7 -7
- sqlspec/adapters/asyncmy/__init__.py +7 -1
- sqlspec/adapters/asyncmy/_types.py +1 -1
- sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
- sqlspec/adapters/asyncmy/adk/store.py +503 -0
- sqlspec/adapters/asyncmy/config.py +59 -17
- sqlspec/adapters/asyncmy/data_dictionary.py +41 -2
- sqlspec/adapters/asyncmy/driver.py +293 -62
- sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
- sqlspec/adapters/asyncmy/litestar/store.py +296 -0
- sqlspec/adapters/asyncpg/__init__.py +2 -1
- sqlspec/adapters/asyncpg/_type_handlers.py +71 -0
- sqlspec/adapters/asyncpg/_types.py +11 -7
- sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
- sqlspec/adapters/asyncpg/adk/store.py +460 -0
- sqlspec/adapters/asyncpg/config.py +57 -36
- sqlspec/adapters/asyncpg/data_dictionary.py +48 -2
- sqlspec/adapters/asyncpg/driver.py +153 -23
- sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
- sqlspec/adapters/asyncpg/litestar/store.py +253 -0
- sqlspec/adapters/bigquery/_types.py +1 -1
- sqlspec/adapters/bigquery/adk/__init__.py +5 -0
- sqlspec/adapters/bigquery/adk/store.py +585 -0
- sqlspec/adapters/bigquery/config.py +36 -11
- sqlspec/adapters/bigquery/data_dictionary.py +42 -2
- sqlspec/adapters/bigquery/driver.py +489 -144
- sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
- sqlspec/adapters/bigquery/litestar/store.py +327 -0
- sqlspec/adapters/bigquery/type_converter.py +55 -23
- sqlspec/adapters/duckdb/_types.py +2 -2
- sqlspec/adapters/duckdb/adk/__init__.py +14 -0
- sqlspec/adapters/duckdb/adk/store.py +563 -0
- sqlspec/adapters/duckdb/config.py +79 -21
- sqlspec/adapters/duckdb/data_dictionary.py +41 -2
- sqlspec/adapters/duckdb/driver.py +225 -44
- sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
- sqlspec/adapters/duckdb/litestar/store.py +332 -0
- sqlspec/adapters/duckdb/pool.py +5 -5
- sqlspec/adapters/duckdb/type_converter.py +51 -21
- sqlspec/adapters/oracledb/_numpy_handlers.py +133 -0
- sqlspec/adapters/oracledb/_types.py +20 -2
- sqlspec/adapters/oracledb/adk/__init__.py +5 -0
- sqlspec/adapters/oracledb/adk/store.py +1628 -0
- sqlspec/adapters/oracledb/config.py +120 -36
- sqlspec/adapters/oracledb/data_dictionary.py +87 -20
- sqlspec/adapters/oracledb/driver.py +475 -86
- sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
- sqlspec/adapters/oracledb/litestar/store.py +765 -0
- sqlspec/adapters/oracledb/migrations.py +316 -25
- sqlspec/adapters/oracledb/type_converter.py +91 -16
- sqlspec/adapters/psqlpy/_type_handlers.py +44 -0
- sqlspec/adapters/psqlpy/_types.py +2 -1
- sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
- sqlspec/adapters/psqlpy/adk/store.py +483 -0
- sqlspec/adapters/psqlpy/config.py +45 -19
- sqlspec/adapters/psqlpy/data_dictionary.py +48 -2
- sqlspec/adapters/psqlpy/driver.py +108 -41
- sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
- sqlspec/adapters/psqlpy/litestar/store.py +272 -0
- sqlspec/adapters/psqlpy/type_converter.py +40 -11
- sqlspec/adapters/psycopg/_type_handlers.py +80 -0
- sqlspec/adapters/psycopg/_types.py +2 -1
- sqlspec/adapters/psycopg/adk/__init__.py +5 -0
- sqlspec/adapters/psycopg/adk/store.py +962 -0
- sqlspec/adapters/psycopg/config.py +65 -37
- sqlspec/adapters/psycopg/data_dictionary.py +91 -3
- sqlspec/adapters/psycopg/driver.py +200 -78
- sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
- sqlspec/adapters/psycopg/litestar/store.py +554 -0
- sqlspec/adapters/sqlite/__init__.py +2 -1
- sqlspec/adapters/sqlite/_type_handlers.py +86 -0
- sqlspec/adapters/sqlite/_types.py +1 -1
- sqlspec/adapters/sqlite/adk/__init__.py +5 -0
- sqlspec/adapters/sqlite/adk/store.py +582 -0
- sqlspec/adapters/sqlite/config.py +85 -16
- sqlspec/adapters/sqlite/data_dictionary.py +34 -2
- sqlspec/adapters/sqlite/driver.py +120 -52
- sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/sqlite/litestar/store.py +318 -0
- sqlspec/adapters/sqlite/pool.py +5 -5
- sqlspec/base.py +45 -26
- sqlspec/builder/__init__.py +73 -4
- sqlspec/builder/_base.py +91 -58
- sqlspec/builder/_column.py +5 -5
- sqlspec/builder/_ddl.py +98 -89
- sqlspec/builder/_delete.py +5 -4
- sqlspec/builder/_dml.py +388 -0
- sqlspec/{_sql.py → builder/_factory.py} +41 -44
- sqlspec/builder/_insert.py +5 -82
- sqlspec/builder/{mixins/_join_operations.py → _join.py} +145 -143
- sqlspec/builder/_merge.py +446 -11
- sqlspec/builder/_parsing_utils.py +9 -11
- sqlspec/builder/_select.py +1313 -25
- sqlspec/builder/_update.py +11 -42
- sqlspec/cli.py +76 -69
- sqlspec/config.py +331 -62
- sqlspec/core/__init__.py +5 -4
- sqlspec/core/cache.py +18 -18
- sqlspec/core/compiler.py +6 -8
- sqlspec/core/filters.py +55 -47
- sqlspec/core/hashing.py +9 -9
- sqlspec/core/parameters.py +76 -45
- sqlspec/core/result.py +234 -47
- sqlspec/core/splitter.py +16 -17
- sqlspec/core/statement.py +32 -31
- sqlspec/core/type_conversion.py +3 -2
- sqlspec/driver/__init__.py +1 -3
- sqlspec/driver/_async.py +183 -160
- sqlspec/driver/_common.py +197 -109
- sqlspec/driver/_sync.py +189 -161
- sqlspec/driver/mixins/_result_tools.py +20 -236
- sqlspec/driver/mixins/_sql_translator.py +4 -4
- sqlspec/exceptions.py +70 -7
- sqlspec/extensions/adk/__init__.py +53 -0
- sqlspec/extensions/adk/_types.py +51 -0
- sqlspec/extensions/adk/converters.py +172 -0
- sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +144 -0
- sqlspec/extensions/adk/migrations/__init__.py +0 -0
- sqlspec/extensions/adk/service.py +181 -0
- sqlspec/extensions/adk/store.py +536 -0
- sqlspec/extensions/aiosql/adapter.py +69 -61
- sqlspec/extensions/fastapi/__init__.py +21 -0
- sqlspec/extensions/fastapi/extension.py +331 -0
- sqlspec/extensions/fastapi/providers.py +543 -0
- sqlspec/extensions/flask/__init__.py +36 -0
- sqlspec/extensions/flask/_state.py +71 -0
- sqlspec/extensions/flask/_utils.py +40 -0
- sqlspec/extensions/flask/extension.py +389 -0
- sqlspec/extensions/litestar/__init__.py +21 -4
- sqlspec/extensions/litestar/cli.py +54 -10
- sqlspec/extensions/litestar/config.py +56 -266
- sqlspec/extensions/litestar/handlers.py +46 -17
- sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
- sqlspec/extensions/litestar/migrations/__init__.py +3 -0
- sqlspec/extensions/litestar/plugin.py +349 -224
- sqlspec/extensions/litestar/providers.py +25 -25
- sqlspec/extensions/litestar/store.py +265 -0
- sqlspec/extensions/starlette/__init__.py +10 -0
- sqlspec/extensions/starlette/_state.py +25 -0
- sqlspec/extensions/starlette/_utils.py +52 -0
- sqlspec/extensions/starlette/extension.py +254 -0
- sqlspec/extensions/starlette/middleware.py +154 -0
- sqlspec/loader.py +30 -49
- sqlspec/migrations/base.py +200 -76
- sqlspec/migrations/commands.py +591 -62
- sqlspec/migrations/context.py +6 -9
- sqlspec/migrations/fix.py +199 -0
- sqlspec/migrations/loaders.py +47 -19
- sqlspec/migrations/runner.py +241 -75
- sqlspec/migrations/tracker.py +237 -21
- sqlspec/migrations/utils.py +51 -3
- sqlspec/migrations/validation.py +177 -0
- sqlspec/protocols.py +106 -36
- sqlspec/storage/_utils.py +85 -0
- sqlspec/storage/backends/fsspec.py +133 -107
- sqlspec/storage/backends/local.py +78 -51
- sqlspec/storage/backends/obstore.py +276 -168
- sqlspec/storage/registry.py +75 -39
- sqlspec/typing.py +30 -84
- sqlspec/utils/__init__.py +25 -4
- sqlspec/utils/arrow_helpers.py +81 -0
- sqlspec/utils/config_resolver.py +6 -6
- sqlspec/utils/correlation.py +4 -5
- sqlspec/utils/data_transformation.py +3 -2
- sqlspec/utils/deprecation.py +9 -8
- sqlspec/utils/fixtures.py +4 -4
- sqlspec/utils/logging.py +46 -6
- sqlspec/utils/module_loader.py +205 -5
- sqlspec/utils/portal.py +311 -0
- sqlspec/utils/schema.py +288 -0
- sqlspec/utils/serializers.py +113 -4
- sqlspec/utils/sync_tools.py +36 -22
- sqlspec/utils/text.py +1 -2
- sqlspec/utils/type_guards.py +136 -20
- sqlspec/utils/version.py +433 -0
- {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/METADATA +41 -22
- sqlspec-0.28.0.dist-info/RECORD +221 -0
- sqlspec/builder/mixins/__init__.py +0 -55
- sqlspec/builder/mixins/_cte_and_set_ops.py +0 -253
- sqlspec/builder/mixins/_delete_operations.py +0 -50
- sqlspec/builder/mixins/_insert_operations.py +0 -282
- sqlspec/builder/mixins/_merge_operations.py +0 -698
- sqlspec/builder/mixins/_order_limit_operations.py +0 -145
- sqlspec/builder/mixins/_pivot_operations.py +0 -157
- sqlspec/builder/mixins/_select_operations.py +0 -930
- sqlspec/builder/mixins/_update_operations.py +0 -199
- sqlspec/builder/mixins/_where_clause.py +0 -1298
- sqlspec-0.26.0.dist-info/RECORD +0 -157
- sqlspec-0.26.0.dist-info/licenses/NOTICE +0 -29
- {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/licenses/LICENSE +0 -0
sqlspec/utils/schema.py
ADDED
|
@@ -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)
|
sqlspec/utils/serializers.py
CHANGED
|
@@ -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,
|
|
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) ->
|
|
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:
|
|
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
|
-
|
|
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")
|
sqlspec/utils/sync_tools.py
CHANGED
|
@@ -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,
|
|
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:
|
|
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
|
|
54
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
135
|
-
If
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
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,
|
|
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
|
-
|
|
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: "
|
|
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:
|
|
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:
|