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.
- sqlspec/__init__.py +7 -15
- sqlspec/_serialization.py +256 -24
- sqlspec/_typing.py +71 -52
- sqlspec/adapters/adbc/_types.py +1 -1
- sqlspec/adapters/adbc/adk/__init__.py +5 -0
- sqlspec/adapters/adbc/adk/store.py +870 -0
- sqlspec/adapters/adbc/config.py +69 -12
- sqlspec/adapters/adbc/data_dictionary.py +340 -0
- sqlspec/adapters/adbc/driver.py +266 -58
- sqlspec/adapters/adbc/litestar/__init__.py +5 -0
- sqlspec/adapters/adbc/litestar/store.py +504 -0
- sqlspec/adapters/adbc/type_converter.py +153 -0
- sqlspec/adapters/aiosqlite/_types.py +1 -1
- sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/adk/store.py +527 -0
- sqlspec/adapters/aiosqlite/config.py +88 -15
- sqlspec/adapters/aiosqlite/data_dictionary.py +149 -0
- sqlspec/adapters/aiosqlite/driver.py +143 -40
- 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 +2 -2
- sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
- sqlspec/adapters/asyncmy/adk/store.py +493 -0
- sqlspec/adapters/asyncmy/config.py +68 -23
- sqlspec/adapters/asyncmy/data_dictionary.py +161 -0
- sqlspec/adapters/asyncmy/driver.py +313 -58
- 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 +450 -0
- sqlspec/adapters/asyncpg/config.py +59 -35
- sqlspec/adapters/asyncpg/data_dictionary.py +173 -0
- sqlspec/adapters/asyncpg/driver.py +170 -25
- 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 +576 -0
- sqlspec/adapters/bigquery/config.py +27 -10
- sqlspec/adapters/bigquery/data_dictionary.py +149 -0
- sqlspec/adapters/bigquery/driver.py +368 -142
- sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
- sqlspec/adapters/bigquery/litestar/store.py +327 -0
- sqlspec/adapters/bigquery/type_converter.py +125 -0
- sqlspec/adapters/duckdb/_types.py +1 -1
- sqlspec/adapters/duckdb/adk/__init__.py +14 -0
- sqlspec/adapters/duckdb/adk/store.py +553 -0
- sqlspec/adapters/duckdb/config.py +80 -20
- sqlspec/adapters/duckdb/data_dictionary.py +163 -0
- sqlspec/adapters/duckdb/driver.py +167 -45
- sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
- sqlspec/adapters/duckdb/litestar/store.py +332 -0
- sqlspec/adapters/duckdb/pool.py +4 -4
- sqlspec/adapters/duckdb/type_converter.py +133 -0
- 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 +1745 -0
- sqlspec/adapters/oracledb/config.py +122 -32
- sqlspec/adapters/oracledb/data_dictionary.py +509 -0
- sqlspec/adapters/oracledb/driver.py +353 -91
- sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
- sqlspec/adapters/oracledb/litestar/store.py +767 -0
- sqlspec/adapters/oracledb/migrations.py +348 -73
- sqlspec/adapters/oracledb/type_converter.py +207 -0
- 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 +482 -0
- sqlspec/adapters/psqlpy/config.py +46 -17
- sqlspec/adapters/psqlpy/data_dictionary.py +172 -0
- sqlspec/adapters/psqlpy/driver.py +123 -209
- sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
- sqlspec/adapters/psqlpy/litestar/store.py +272 -0
- sqlspec/adapters/psqlpy/type_converter.py +102 -0
- 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 +944 -0
- sqlspec/adapters/psycopg/config.py +69 -35
- sqlspec/adapters/psycopg/data_dictionary.py +331 -0
- sqlspec/adapters/psycopg/driver.py +238 -81
- 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 +572 -0
- sqlspec/adapters/sqlite/config.py +87 -15
- sqlspec/adapters/sqlite/data_dictionary.py +149 -0
- sqlspec/adapters/sqlite/driver.py +137 -54
- sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/sqlite/litestar/store.py +318 -0
- sqlspec/adapters/sqlite/pool.py +18 -9
- sqlspec/base.py +45 -26
- sqlspec/builder/__init__.py +73 -4
- sqlspec/builder/_base.py +162 -89
- sqlspec/builder/_column.py +62 -29
- sqlspec/builder/_ddl.py +180 -121
- sqlspec/builder/_delete.py +5 -4
- sqlspec/builder/_dml.py +388 -0
- sqlspec/{_sql.py → builder/_factory.py} +53 -94
- sqlspec/builder/_insert.py +32 -131
- sqlspec/builder/_join.py +375 -0
- sqlspec/builder/_merge.py +446 -11
- sqlspec/builder/_parsing_utils.py +111 -17
- sqlspec/builder/_select.py +1457 -24
- sqlspec/builder/_update.py +11 -42
- sqlspec/cli.py +307 -194
- sqlspec/config.py +252 -67
- sqlspec/core/__init__.py +5 -4
- sqlspec/core/cache.py +17 -17
- sqlspec/core/compiler.py +62 -9
- sqlspec/core/filters.py +37 -37
- sqlspec/core/hashing.py +9 -9
- sqlspec/core/parameters.py +83 -48
- sqlspec/core/result.py +102 -46
- sqlspec/core/splitter.py +16 -17
- sqlspec/core/statement.py +36 -30
- sqlspec/core/type_conversion.py +235 -0
- sqlspec/driver/__init__.py +7 -6
- sqlspec/driver/_async.py +188 -151
- sqlspec/driver/_common.py +285 -80
- sqlspec/driver/_sync.py +188 -152
- sqlspec/driver/mixins/_result_tools.py +20 -236
- sqlspec/driver/mixins/_sql_translator.py +4 -4
- sqlspec/exceptions.py +75 -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 +73 -53
- sqlspec/extensions/litestar/__init__.py +21 -4
- sqlspec/extensions/litestar/cli.py +54 -10
- sqlspec/extensions/litestar/config.py +59 -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 +324 -223
- sqlspec/extensions/litestar/providers.py +25 -25
- sqlspec/extensions/litestar/store.py +265 -0
- sqlspec/loader.py +30 -49
- sqlspec/migrations/__init__.py +4 -3
- sqlspec/migrations/base.py +302 -39
- sqlspec/migrations/commands.py +611 -144
- sqlspec/migrations/context.py +142 -0
- sqlspec/migrations/fix.py +199 -0
- sqlspec/migrations/loaders.py +68 -23
- sqlspec/migrations/runner.py +543 -107
- sqlspec/migrations/tracker.py +237 -21
- sqlspec/migrations/utils.py +51 -3
- sqlspec/migrations/validation.py +177 -0
- sqlspec/protocols.py +66 -36
- sqlspec/storage/_utils.py +98 -0
- sqlspec/storage/backends/fsspec.py +134 -106
- sqlspec/storage/backends/local.py +78 -51
- sqlspec/storage/backends/obstore.py +278 -162
- sqlspec/storage/registry.py +75 -39
- sqlspec/typing.py +16 -84
- sqlspec/utils/config_resolver.py +153 -0
- 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 +2 -2
- sqlspec/utils/schema.py +288 -0
- sqlspec/utils/serializers.py +50 -2
- sqlspec/utils/sync_tools.py +21 -17
- sqlspec/utils/text.py +1 -2
- sqlspec/utils/type_guards.py +111 -20
- sqlspec/utils/version.py +433 -0
- {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/METADATA +40 -21
- sqlspec-0.27.0.dist-info/RECORD +207 -0
- sqlspec/builder/mixins/__init__.py +0 -55
- sqlspec/builder/mixins/_cte_and_set_ops.py +0 -254
- sqlspec/builder/mixins/_delete_operations.py +0 -50
- sqlspec/builder/mixins/_insert_operations.py +0 -282
- sqlspec/builder/mixins/_join_operations.py +0 -389
- sqlspec/builder/mixins/_merge_operations.py +0 -592
- sqlspec/builder/mixins/_order_limit_operations.py +0 -152
- sqlspec/builder/mixins/_pivot_operations.py +0 -157
- sqlspec/builder/mixins/_select_operations.py +0 -936
- sqlspec/builder/mixins/_update_operations.py +0 -218
- sqlspec/builder/mixins/_where_clause.py +0 -1304
- sqlspec-0.25.0.dist-info/RECORD +0 -139
- sqlspec-0.25.0.dist-info/licenses/NOTICE +0 -29
- {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/entry_points.txt +0 -0
- {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
|
|
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
|
|
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: "
|
|
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: "
|
|
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
|
|
11
|
+
from typing import Any
|
|
12
12
|
|
|
13
13
|
from sqlspec._serialization import encode_json
|
|
14
14
|
|
|
15
|
-
__all__ = (
|
|
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[
|
|
25
|
+
correlation_id_var: "ContextVar[str | None]" = ContextVar("correlation_id", default=None)
|
|
18
26
|
|
|
19
27
|
|
|
20
|
-
def set_correlation_id(correlation_id: "
|
|
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() -> "
|
|
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
|
-
|
|
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())
|
sqlspec/utils/module_loader.py
CHANGED
|
@@ -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
|
|
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: "
|
|
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)
|
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
|
@@ -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
|
|
8
|
-
|
|
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")
|
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]":
|
|
@@ -169,7 +176,7 @@ def await_(
|
|
|
169
176
|
|
|
170
177
|
|
|
171
178
|
def async_(
|
|
172
|
-
function: "Callable[ParamSpecT, ReturnT]", *, limiter: "
|
|
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,
|
|
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
|
-
|
|
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: "
|
|
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:
|
|
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:
|