sqlspec 0.21.0__py3-none-any.whl → 0.22.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/base.py +4 -4
- sqlspec/driver/mixins/_result_tools.py +41 -6
- sqlspec/loader.py +65 -68
- sqlspec/protocols.py +3 -5
- sqlspec/storage/__init__.py +2 -12
- sqlspec/storage/backends/__init__.py +1 -0
- sqlspec/storage/backends/fsspec.py +87 -147
- sqlspec/storage/backends/local.py +310 -0
- sqlspec/storage/backends/obstore.py +210 -192
- sqlspec/storage/registry.py +101 -70
- sqlspec/utils/data_transformation.py +120 -0
- sqlspec/utils/sync_tools.py +8 -5
- sqlspec/utils/text.py +27 -19
- sqlspec/utils/type_guards.py +74 -0
- {sqlspec-0.21.0.dist-info → sqlspec-0.22.0.dist-info}/METADATA +1 -1
- {sqlspec-0.21.0.dist-info → sqlspec-0.22.0.dist-info}/RECORD +20 -19
- sqlspec/storage/capabilities.py +0 -102
- {sqlspec-0.21.0.dist-info → sqlspec-0.22.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.21.0.dist-info → sqlspec-0.22.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.21.0.dist-info → sqlspec-0.22.0.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.21.0.dist-info → sqlspec-0.22.0.dist-info}/licenses/NOTICE +0 -0
sqlspec/storage/registry.py
CHANGED
|
@@ -14,7 +14,6 @@ from mypy_extensions import mypyc_attr
|
|
|
14
14
|
|
|
15
15
|
from sqlspec.exceptions import ImproperConfigurationError, MissingDependencyError
|
|
16
16
|
from sqlspec.protocols import ObjectStoreProtocol
|
|
17
|
-
from sqlspec.storage.capabilities import StorageCapabilities
|
|
18
17
|
from sqlspec.typing import FSSPEC_INSTALLED, OBSTORE_INSTALLED
|
|
19
18
|
|
|
20
19
|
__all__ = ("StorageRegistry", "storage_registry")
|
|
@@ -22,34 +21,52 @@ __all__ = ("StorageRegistry", "storage_registry")
|
|
|
22
21
|
logger = logging.getLogger(__name__)
|
|
23
22
|
|
|
24
23
|
|
|
24
|
+
def _is_local_uri(uri: str) -> bool:
|
|
25
|
+
"""Check if URI represents a local filesystem path."""
|
|
26
|
+
if "://" in uri and not uri.startswith("file://"):
|
|
27
|
+
return False
|
|
28
|
+
windows_drive_min_length = 3
|
|
29
|
+
return (
|
|
30
|
+
Path(uri).exists()
|
|
31
|
+
or Path(uri).is_absolute()
|
|
32
|
+
or uri.startswith(("~", ".", "/"))
|
|
33
|
+
or (len(uri) >= windows_drive_min_length and uri[1:3] == ":\\")
|
|
34
|
+
or "/" in uri
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
25
38
|
SCHEME_REGEX: Final = re.compile(r"([a-zA-Z0-9+.-]+)://")
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
GCS_PROTOCOL: Final[str] = "gs"
|
|
29
|
-
AZURE_PROTOCOL: Final[str] = "az"
|
|
39
|
+
|
|
40
|
+
|
|
30
41
|
FSSPEC_ONLY_SCHEMES: Final[frozenset[str]] = frozenset({"http", "https", "ftp", "sftp", "ssh"})
|
|
31
42
|
|
|
32
43
|
|
|
33
44
|
@mypyc_attr(allow_interpreted_subclasses=True)
|
|
34
45
|
class StorageRegistry:
|
|
35
|
-
"""
|
|
46
|
+
"""Global storage registry for named backend configurations.
|
|
36
47
|
|
|
37
|
-
|
|
38
|
-
|
|
48
|
+
Allows registering named storage backends that can be accessed from anywhere
|
|
49
|
+
in your application. Backends are automatically selected based on URI scheme
|
|
50
|
+
unless explicitly overridden.
|
|
39
51
|
|
|
40
52
|
Examples:
|
|
41
|
-
|
|
42
|
-
backend = registry.get("
|
|
43
|
-
backend = registry.get("
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
+
# Direct URI access to storage containers
|
|
54
|
+
backend = registry.get("s3://my-bucket")
|
|
55
|
+
backend = registry.get("file:///tmp/data")
|
|
56
|
+
backend = registry.get("gs://my-gcs-bucket")
|
|
57
|
+
|
|
58
|
+
# Named store pattern for environment-specific backends
|
|
59
|
+
# Development
|
|
60
|
+
registry.register_alias("my_app_store", "file:///tmp/dev_data")
|
|
61
|
+
|
|
62
|
+
# Production
|
|
63
|
+
registry.register_alias("my_app_store", "s3://prod-bucket/data")
|
|
64
|
+
|
|
65
|
+
# Access from anywhere in your app
|
|
66
|
+
store = registry.get("my_app_store") # Works in both environments
|
|
67
|
+
|
|
68
|
+
# Force specific backend when multiple options available
|
|
69
|
+
backend = registry.get("s3://bucket", backend="fsspec") # Force fsspec over obstore
|
|
53
70
|
"""
|
|
54
71
|
|
|
55
72
|
__slots__ = ("_alias_configs", "_aliases", "_cache", "_instances")
|
|
@@ -60,44 +77,47 @@ class StorageRegistry:
|
|
|
60
77
|
self._instances: dict[Union[str, tuple[str, tuple[tuple[str, Any], ...]]], ObjectStoreProtocol] = {}
|
|
61
78
|
self._cache: dict[str, tuple[str, type[ObjectStoreProtocol]]] = {}
|
|
62
79
|
|
|
80
|
+
def _make_hashable(self, obj: Any) -> Any:
|
|
81
|
+
"""Convert nested dict/list structures to hashable tuples."""
|
|
82
|
+
if isinstance(obj, dict):
|
|
83
|
+
return tuple(sorted((k, self._make_hashable(v)) for k, v in obj.items()))
|
|
84
|
+
if isinstance(obj, list):
|
|
85
|
+
return tuple(self._make_hashable(item) for item in obj)
|
|
86
|
+
if isinstance(obj, set):
|
|
87
|
+
return tuple(sorted(self._make_hashable(item) for item in obj))
|
|
88
|
+
return obj
|
|
89
|
+
|
|
63
90
|
def register_alias(
|
|
64
|
-
self,
|
|
65
|
-
alias: str,
|
|
66
|
-
uri: str,
|
|
67
|
-
*,
|
|
68
|
-
backend: Optional[type[ObjectStoreProtocol]] = None,
|
|
69
|
-
base_path: str = "",
|
|
70
|
-
config: Optional[dict[str, Any]] = None,
|
|
71
|
-
**kwargs: Any,
|
|
91
|
+
self, alias: str, uri: str, *, backend: Optional[str] = None, base_path: str = "", **kwargs: Any
|
|
72
92
|
) -> None:
|
|
73
93
|
"""Register a named alias for a storage configuration.
|
|
74
94
|
|
|
75
95
|
Args:
|
|
76
|
-
alias: Unique alias name
|
|
77
|
-
uri: Storage URI (e.g., "s3://bucket", "file:///path")
|
|
78
|
-
backend:
|
|
96
|
+
alias: Unique alias name (e.g., "my_app_store", "user_uploads")
|
|
97
|
+
uri: Storage URI (e.g., "s3://bucket", "file:///path", "gs://bucket")
|
|
98
|
+
backend: Force specific backend ("local", "fsspec", "obstore") instead of auto-detection
|
|
79
99
|
base_path: Base path to prepend to all operations
|
|
80
|
-
config: Additional configuration dict
|
|
81
100
|
**kwargs: Backend-specific configuration options
|
|
82
101
|
"""
|
|
83
|
-
if backend
|
|
84
|
-
backend = self._determine_backend_class(uri)
|
|
102
|
+
backend_cls = self._get_backend_class(backend) if backend else self._determine_backend_class(uri)
|
|
85
103
|
|
|
86
|
-
|
|
87
|
-
config.update(kwargs)
|
|
88
|
-
backend_config = dict(config)
|
|
104
|
+
backend_config = dict(kwargs)
|
|
89
105
|
if base_path:
|
|
90
106
|
backend_config["base_path"] = base_path
|
|
91
|
-
self._alias_configs[alias] = (
|
|
107
|
+
self._alias_configs[alias] = (backend_cls, uri, backend_config)
|
|
108
|
+
|
|
92
109
|
test_config = dict(backend_config)
|
|
93
110
|
test_config["uri"] = uri
|
|
94
111
|
self._aliases[alias] = test_config
|
|
95
112
|
|
|
96
|
-
def get(
|
|
113
|
+
def get(
|
|
114
|
+
self, uri_or_alias: Union[str, Path], *, backend: Optional[str] = None, **kwargs: Any
|
|
115
|
+
) -> ObjectStoreProtocol:
|
|
97
116
|
"""Get backend instance using URI-first routing with automatic backend selection.
|
|
98
117
|
|
|
99
118
|
Args:
|
|
100
|
-
uri_or_alias: URI to resolve directly OR named alias
|
|
119
|
+
uri_or_alias: URI to resolve directly OR named alias (e.g., "my_app_store")
|
|
120
|
+
backend: Force specific backend ("local", "fsspec", "obstore") instead of auto-selection
|
|
101
121
|
**kwargs: Additional backend-specific configuration options
|
|
102
122
|
|
|
103
123
|
Returns:
|
|
@@ -113,24 +133,20 @@ class StorageRegistry:
|
|
|
113
133
|
if isinstance(uri_or_alias, Path):
|
|
114
134
|
uri_or_alias = f"file://{uri_or_alias.resolve()}"
|
|
115
135
|
|
|
116
|
-
cache_key = (uri_or_alias,
|
|
136
|
+
cache_key = (uri_or_alias, self._make_hashable(kwargs)) if kwargs else uri_or_alias
|
|
117
137
|
if cache_key in self._instances:
|
|
118
138
|
return self._instances[cache_key]
|
|
119
139
|
scheme = self._get_scheme(uri_or_alias)
|
|
120
|
-
if not scheme and (
|
|
121
|
-
Path(uri_or_alias).exists()
|
|
122
|
-
or Path(uri_or_alias).is_absolute()
|
|
123
|
-
or uri_or_alias.startswith(("~", "."))
|
|
124
|
-
or ":\\" in uri_or_alias
|
|
125
|
-
or "/" in uri_or_alias
|
|
126
|
-
):
|
|
140
|
+
if not scheme and _is_local_uri(uri_or_alias):
|
|
127
141
|
scheme = "file"
|
|
128
142
|
uri_or_alias = f"file://{uri_or_alias}"
|
|
129
143
|
|
|
130
144
|
if scheme:
|
|
131
|
-
instance = self._resolve_from_uri(uri_or_alias, **kwargs)
|
|
145
|
+
instance = self._resolve_from_uri(uri_or_alias, backend_override=backend, **kwargs)
|
|
132
146
|
elif uri_or_alias in self._alias_configs:
|
|
133
147
|
backend_cls, stored_uri, config = self._alias_configs[uri_or_alias]
|
|
148
|
+
if backend:
|
|
149
|
+
backend_cls = self._get_backend_class(backend)
|
|
134
150
|
instance = backend_cls(stored_uri, **{**config, **kwargs})
|
|
135
151
|
else:
|
|
136
152
|
msg = f"Unknown storage alias or invalid URI: '{uri_or_alias}'"
|
|
@@ -138,36 +154,66 @@ class StorageRegistry:
|
|
|
138
154
|
self._instances[cache_key] = instance
|
|
139
155
|
return instance
|
|
140
156
|
|
|
141
|
-
def _resolve_from_uri(
|
|
142
|
-
|
|
157
|
+
def _resolve_from_uri(
|
|
158
|
+
self, uri: str, *, backend_override: Optional[str] = None, **kwargs: Any
|
|
159
|
+
) -> ObjectStoreProtocol:
|
|
160
|
+
"""Resolve backend from URI with optional backend override."""
|
|
161
|
+
if backend_override:
|
|
162
|
+
return self._create_backend(backend_override, uri, **kwargs)
|
|
143
163
|
scheme = self._get_scheme(uri)
|
|
164
|
+
|
|
165
|
+
# For local files, prefer LocalStore first
|
|
166
|
+
if scheme in {None, "file"}:
|
|
167
|
+
return self._create_backend("local", uri, **kwargs)
|
|
168
|
+
|
|
169
|
+
# Try ObStore first if available and appropriate
|
|
144
170
|
if scheme not in FSSPEC_ONLY_SCHEMES and OBSTORE_INSTALLED:
|
|
145
171
|
try:
|
|
146
172
|
return self._create_backend("obstore", uri, **kwargs)
|
|
147
173
|
except (ValueError, ImportError, NotImplementedError):
|
|
148
174
|
pass
|
|
175
|
+
|
|
176
|
+
# Try FSSpec if available
|
|
149
177
|
if FSSPEC_INSTALLED:
|
|
150
178
|
try:
|
|
151
179
|
return self._create_backend("fsspec", uri, **kwargs)
|
|
152
180
|
except (ValueError, ImportError, NotImplementedError):
|
|
153
181
|
pass
|
|
154
|
-
|
|
155
|
-
|
|
182
|
+
|
|
183
|
+
# For cloud schemes without backends, provide helpful error
|
|
184
|
+
msg = f"No backend available for URI scheme '{scheme}'. Install obstore or fsspec for cloud storage support."
|
|
185
|
+
raise MissingDependencyError(msg)
|
|
156
186
|
|
|
157
187
|
def _determine_backend_class(self, uri: str) -> type[ObjectStoreProtocol]:
|
|
158
188
|
"""Determine the backend class for a URI based on availability."""
|
|
159
189
|
scheme = self._get_scheme(uri)
|
|
190
|
+
|
|
191
|
+
# For local files, always use LocalStore
|
|
192
|
+
if scheme in {None, "file"}:
|
|
193
|
+
return self._get_backend_class("local")
|
|
194
|
+
|
|
195
|
+
# FSSpec-only schemes require FSSpec
|
|
160
196
|
if scheme in FSSPEC_ONLY_SCHEMES and FSSPEC_INSTALLED:
|
|
161
197
|
return self._get_backend_class("fsspec")
|
|
198
|
+
|
|
199
|
+
# Prefer ObStore for cloud storage if available
|
|
162
200
|
if OBSTORE_INSTALLED:
|
|
163
201
|
return self._get_backend_class("obstore")
|
|
202
|
+
|
|
203
|
+
# Fall back to FSSpec if available
|
|
164
204
|
if FSSPEC_INSTALLED:
|
|
165
205
|
return self._get_backend_class("fsspec")
|
|
166
|
-
|
|
206
|
+
|
|
207
|
+
# For cloud schemes without backends, provide helpful error
|
|
208
|
+
msg = f"No backend available for URI scheme '{scheme}'. Install obstore or fsspec for cloud storage support."
|
|
167
209
|
raise MissingDependencyError(msg)
|
|
168
210
|
|
|
169
211
|
def _get_backend_class(self, backend_type: str) -> type[ObjectStoreProtocol]:
|
|
170
212
|
"""Get backend class by type name."""
|
|
213
|
+
if backend_type == "local":
|
|
214
|
+
from sqlspec.storage.backends.local import LocalStore
|
|
215
|
+
|
|
216
|
+
return cast("type[ObjectStoreProtocol]", LocalStore)
|
|
171
217
|
if backend_type == "obstore":
|
|
172
218
|
from sqlspec.storage.backends.obstore import ObStoreBackend
|
|
173
219
|
|
|
@@ -176,7 +222,7 @@ class StorageRegistry:
|
|
|
176
222
|
from sqlspec.storage.backends.fsspec import FSSpecBackend
|
|
177
223
|
|
|
178
224
|
return cast("type[ObjectStoreProtocol]", FSSpecBackend)
|
|
179
|
-
msg = f"Unknown backend type: {backend_type}. Supported types: 'obstore', 'fsspec'"
|
|
225
|
+
msg = f"Unknown backend type: {backend_type}. Supported types: 'local', 'obstore', 'fsspec'"
|
|
180
226
|
raise ValueError(msg)
|
|
181
227
|
|
|
182
228
|
def _create_backend(self, backend_type: str, uri: str, **kwargs: Any) -> ObjectStoreProtocol:
|
|
@@ -220,20 +266,5 @@ class StorageRegistry:
|
|
|
220
266
|
self._alias_configs.clear()
|
|
221
267
|
self._aliases.clear()
|
|
222
268
|
|
|
223
|
-
def get_backend_capabilities(self, uri_or_alias: Union[str, Path]) -> "StorageCapabilities":
|
|
224
|
-
"""Get capabilities for a backend without creating an instance."""
|
|
225
|
-
if isinstance(uri_or_alias, Path):
|
|
226
|
-
uri_or_alias = f"file://{uri_or_alias.resolve()}"
|
|
227
|
-
if "://" in uri_or_alias:
|
|
228
|
-
backend_cls = self._determine_backend_class(uri_or_alias)
|
|
229
|
-
elif uri_or_alias in self._alias_configs:
|
|
230
|
-
backend_cls, _, _ = self._alias_configs[uri_or_alias]
|
|
231
|
-
else:
|
|
232
|
-
msg = f"Unknown storage alias or invalid URI: '{uri_or_alias}'"
|
|
233
|
-
raise ImproperConfigurationError(msg)
|
|
234
|
-
if hasattr(backend_cls, "capabilities"):
|
|
235
|
-
return backend_cls.capabilities
|
|
236
|
-
return StorageCapabilities()
|
|
237
|
-
|
|
238
269
|
|
|
239
270
|
storage_registry = StorageRegistry()
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Data transformation utilities for SQLSpec.
|
|
2
|
+
|
|
3
|
+
Provides functions for transforming data structures, particularly for
|
|
4
|
+
field name conversion when mapping database results to schema objects.
|
|
5
|
+
Used primarily for msgspec field name conversion with rename configurations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Callable, Union
|
|
9
|
+
|
|
10
|
+
__all__ = ("transform_dict_keys",)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _safe_convert_key(key: Any, converter: Callable[[str], str]) -> Any:
|
|
14
|
+
"""Safely convert a key using the converter function.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
key: Key to convert (may not be a string).
|
|
18
|
+
converter: Function to convert string keys.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
Converted key if conversion succeeds, original key otherwise.
|
|
22
|
+
"""
|
|
23
|
+
if not isinstance(key, str):
|
|
24
|
+
return key
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
return converter(key)
|
|
28
|
+
except (TypeError, ValueError, AttributeError):
|
|
29
|
+
# If conversion fails, return the original key
|
|
30
|
+
return key
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def transform_dict_keys(data: Union[dict, list, Any], converter: Callable[[str], str]) -> Union[dict, list, Any]:
|
|
34
|
+
"""Transform dictionary keys using the provided converter function.
|
|
35
|
+
|
|
36
|
+
Recursively transforms all dictionary keys in a data structure using
|
|
37
|
+
the provided converter function. Handles nested dictionaries, lists
|
|
38
|
+
of dictionaries, and preserves non-dict values unchanged.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
data: The data structure to transform. Can be a dict, list, or any other type.
|
|
42
|
+
converter: Function to convert string keys (e.g., camelize, kebabize).
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
The transformed data structure with converted keys. Non-dict values
|
|
46
|
+
are returned unchanged.
|
|
47
|
+
|
|
48
|
+
Examples:
|
|
49
|
+
Transform snake_case keys to camelCase:
|
|
50
|
+
|
|
51
|
+
>>> from sqlspec.utils.text import camelize
|
|
52
|
+
>>> data = {"user_id": 123, "created_at": "2024-01-01"}
|
|
53
|
+
>>> transform_dict_keys(data, camelize)
|
|
54
|
+
{"userId": 123, "createdAt": "2024-01-01"}
|
|
55
|
+
|
|
56
|
+
Transform nested structures:
|
|
57
|
+
|
|
58
|
+
>>> nested = {
|
|
59
|
+
... "user_data": {"first_name": "John", "last_name": "Doe"},
|
|
60
|
+
... "order_items": [
|
|
61
|
+
... {"item_id": 1, "item_name": "Product A"},
|
|
62
|
+
... {"item_id": 2, "item_name": "Product B"},
|
|
63
|
+
... ],
|
|
64
|
+
... }
|
|
65
|
+
>>> transform_dict_keys(nested, camelize)
|
|
66
|
+
{
|
|
67
|
+
"userData": {
|
|
68
|
+
"firstName": "John",
|
|
69
|
+
"lastName": "Doe"
|
|
70
|
+
},
|
|
71
|
+
"orderItems": [
|
|
72
|
+
{"itemId": 1, "itemName": "Product A"},
|
|
73
|
+
{"itemId": 2, "itemName": "Product B"}
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
"""
|
|
77
|
+
if isinstance(data, dict):
|
|
78
|
+
return _transform_dict(data, converter)
|
|
79
|
+
if isinstance(data, list):
|
|
80
|
+
return _transform_list(data, converter)
|
|
81
|
+
return data
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _transform_dict(data: dict, converter: Callable[[str], str]) -> dict:
|
|
85
|
+
"""Transform a dictionary's keys recursively.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
data: Dictionary to transform.
|
|
89
|
+
converter: Function to convert string keys.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Dictionary with transformed keys and recursively transformed values.
|
|
93
|
+
"""
|
|
94
|
+
transformed = {}
|
|
95
|
+
|
|
96
|
+
for key, value in data.items():
|
|
97
|
+
# Convert the key using the provided converter function
|
|
98
|
+
# Use safe conversion that handles edge cases without try-except
|
|
99
|
+
converted_key = _safe_convert_key(key, converter)
|
|
100
|
+
|
|
101
|
+
# Recursively transform the value
|
|
102
|
+
transformed_value = transform_dict_keys(value, converter)
|
|
103
|
+
|
|
104
|
+
transformed[converted_key] = transformed_value
|
|
105
|
+
|
|
106
|
+
return transformed
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _transform_list(data: list, converter: Callable[[str], str]) -> list:
|
|
110
|
+
"""Transform a list's elements recursively.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
data: List to transform.
|
|
114
|
+
converter: Function to convert string keys in nested structures.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
List with recursively transformed elements.
|
|
118
|
+
"""
|
|
119
|
+
# Use list comprehension for better performance and avoid try-except in loop
|
|
120
|
+
return [transform_dict_keys(item, converter) for item in data]
|
sqlspec/utils/sync_tools.py
CHANGED
|
@@ -29,6 +29,13 @@ ParamSpecT = ParamSpec("ParamSpecT")
|
|
|
29
29
|
T = TypeVar("T")
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
class NoValue:
|
|
33
|
+
"""Sentinel class for missing values."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
NO_VALUE = NoValue()
|
|
37
|
+
|
|
38
|
+
|
|
32
39
|
class CapacityLimiter:
|
|
33
40
|
"""Limits the number of concurrent operations using a semaphore."""
|
|
34
41
|
|
|
@@ -240,11 +247,7 @@ def with_ensure_async_(
|
|
|
240
247
|
return obj
|
|
241
248
|
|
|
242
249
|
|
|
243
|
-
|
|
244
|
-
"""Sentinel class for missing values."""
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
async def get_next(iterable: Any, default: Any = NoValue, *args: Any) -> Any: # pragma: no cover
|
|
250
|
+
async def get_next(iterable: Any, default: Any = NO_VALUE, *args: Any) -> Any: # pragma: no cover
|
|
248
251
|
"""Return the next item from an async iterator.
|
|
249
252
|
|
|
250
253
|
Args:
|
sqlspec/utils/text.py
CHANGED
|
@@ -19,25 +19,7 @@ _SNAKE_CASE_HYPHEN_SPACE = re.compile(r"[.\s@-]+", re.UNICODE)
|
|
|
19
19
|
_SNAKE_CASE_REMOVE_NON_WORD = re.compile(r"[^\w]+", re.UNICODE)
|
|
20
20
|
_SNAKE_CASE_MULTIPLE_UNDERSCORES = re.compile(r"__+", re.UNICODE)
|
|
21
21
|
|
|
22
|
-
__all__ = ("camelize", "
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def check_email(email: str) -> str:
|
|
26
|
-
"""Validate an email address.
|
|
27
|
-
|
|
28
|
-
Args:
|
|
29
|
-
email: The email to validate.
|
|
30
|
-
|
|
31
|
-
Raises:
|
|
32
|
-
ValueError: If the email is invalid.
|
|
33
|
-
|
|
34
|
-
Returns:
|
|
35
|
-
The validated email.
|
|
36
|
-
"""
|
|
37
|
-
if "@" not in email:
|
|
38
|
-
msg = "Invalid email!"
|
|
39
|
-
raise ValueError(msg)
|
|
40
|
-
return email.lower()
|
|
22
|
+
__all__ = ("camelize", "kebabize", "pascalize", "slugify", "snake_case")
|
|
41
23
|
|
|
42
24
|
|
|
43
25
|
def slugify(value: str, allow_unicode: bool = False, separator: Optional[str] = None) -> str:
|
|
@@ -80,6 +62,32 @@ def camelize(string: str) -> str:
|
|
|
80
62
|
return "".join(word if index == 0 else word.capitalize() for index, word in enumerate(string.split("_")))
|
|
81
63
|
|
|
82
64
|
|
|
65
|
+
@lru_cache(maxsize=100)
|
|
66
|
+
def kebabize(string: str) -> str:
|
|
67
|
+
"""Convert a string to kebab-case.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
string: The string to convert.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
The kebab-case version of the string.
|
|
74
|
+
"""
|
|
75
|
+
return "-".join(word.lower() for word in string.split("_") if word)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@lru_cache(maxsize=100)
|
|
79
|
+
def pascalize(string: str) -> str:
|
|
80
|
+
"""Convert a string to PascalCase.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
string: The string to convert.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
The PascalCase version of the string.
|
|
87
|
+
"""
|
|
88
|
+
return "".join(word.capitalize() for word in string.split("_") if word)
|
|
89
|
+
|
|
90
|
+
|
|
83
91
|
@lru_cache(maxsize=100)
|
|
84
92
|
def snake_case(string: str) -> str:
|
|
85
93
|
"""Convert a string to snake_case.
|
sqlspec/utils/type_guards.py
CHANGED
|
@@ -6,6 +6,7 @@ understand type narrowing, replacing defensive hasattr() and duck typing pattern
|
|
|
6
6
|
|
|
7
7
|
from collections.abc import Sequence
|
|
8
8
|
from collections.abc import Set as AbstractSet
|
|
9
|
+
from functools import lru_cache
|
|
9
10
|
from typing import TYPE_CHECKING, Any, Optional, Union, cast
|
|
10
11
|
|
|
11
12
|
from sqlspec.typing import (
|
|
@@ -59,6 +60,7 @@ __all__ = (
|
|
|
59
60
|
"extract_dataclass_items",
|
|
60
61
|
"get_initial_expression",
|
|
61
62
|
"get_literal_parent",
|
|
63
|
+
"get_msgspec_rename_config",
|
|
62
64
|
"get_node_expressions",
|
|
63
65
|
"get_node_this",
|
|
64
66
|
"get_param_style_and_name",
|
|
@@ -429,6 +431,78 @@ def is_msgspec_struct_without_field(obj: Any, field_name: str) -> "TypeGuard[Str
|
|
|
429
431
|
return False
|
|
430
432
|
|
|
431
433
|
|
|
434
|
+
@lru_cache(maxsize=500)
|
|
435
|
+
def _detect_rename_pattern(field_name: str, encode_name: str) -> "Optional[str]":
|
|
436
|
+
"""Detect the rename pattern by comparing field name transformations.
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
field_name: Original field name (e.g., "user_id")
|
|
440
|
+
encode_name: Encoded field name (e.g., "userId")
|
|
441
|
+
|
|
442
|
+
Returns:
|
|
443
|
+
The detected rename pattern ("camel", "kebab", "pascal") or None
|
|
444
|
+
"""
|
|
445
|
+
from sqlspec.utils.text import camelize, kebabize, pascalize
|
|
446
|
+
|
|
447
|
+
# Test camelCase conversion
|
|
448
|
+
if encode_name == camelize(field_name) and encode_name != field_name:
|
|
449
|
+
return "camel"
|
|
450
|
+
|
|
451
|
+
if encode_name == kebabize(field_name) and encode_name != field_name:
|
|
452
|
+
return "kebab"
|
|
453
|
+
|
|
454
|
+
if encode_name == pascalize(field_name) and encode_name != field_name:
|
|
455
|
+
return "pascal"
|
|
456
|
+
return None
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def get_msgspec_rename_config(schema_type: type) -> "Optional[str]":
|
|
460
|
+
"""Extract msgspec rename configuration from a struct type.
|
|
461
|
+
|
|
462
|
+
Analyzes field name transformations to detect the rename pattern used by msgspec.
|
|
463
|
+
Since msgspec doesn't store the original rename parameter directly, we infer it
|
|
464
|
+
by comparing field names with their encode_name values.
|
|
465
|
+
|
|
466
|
+
Args:
|
|
467
|
+
schema_type: The msgspec struct type to inspect.
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
The rename configuration value ("camel", "kebab", "pascal", etc.) if detected,
|
|
471
|
+
None if no rename configuration exists or if not a msgspec struct.
|
|
472
|
+
|
|
473
|
+
Examples:
|
|
474
|
+
>>> class User(msgspec.Struct, rename="camel"):
|
|
475
|
+
... user_id: int
|
|
476
|
+
>>> get_msgspec_rename_config(User)
|
|
477
|
+
"camel"
|
|
478
|
+
|
|
479
|
+
>>> class Product(msgspec.Struct):
|
|
480
|
+
... product_id: int
|
|
481
|
+
>>> get_msgspec_rename_config(Product)
|
|
482
|
+
None
|
|
483
|
+
"""
|
|
484
|
+
if not MSGSPEC_INSTALLED:
|
|
485
|
+
return None
|
|
486
|
+
|
|
487
|
+
if not is_msgspec_struct(schema_type):
|
|
488
|
+
return None
|
|
489
|
+
|
|
490
|
+
from msgspec import structs
|
|
491
|
+
|
|
492
|
+
fields = structs.fields(schema_type) # type: ignore[arg-type]
|
|
493
|
+
if not fields:
|
|
494
|
+
return None
|
|
495
|
+
|
|
496
|
+
# Check if any field name differs from its encode_name
|
|
497
|
+
for field in fields:
|
|
498
|
+
if field.name != field.encode_name:
|
|
499
|
+
# Detect the rename pattern by comparing transformations
|
|
500
|
+
return _detect_rename_pattern(field.name, field.encode_name)
|
|
501
|
+
|
|
502
|
+
# If all field names match their encode_name, no rename is applied
|
|
503
|
+
return None
|
|
504
|
+
|
|
505
|
+
|
|
432
506
|
def is_attrs_instance(obj: Any) -> "TypeGuard[AttrsInstanceStub]":
|
|
433
507
|
"""Check if a value is an attrs class instance.
|
|
434
508
|
|
|
@@ -4,12 +4,12 @@ sqlspec/__metadata__.py,sha256=IUw6MCTy1oeUJ1jAVYbuJLkOWbiAWorZ5W-E-SAD9N4,395
|
|
|
4
4
|
sqlspec/_serialization.py,sha256=6U5-smk2h2yl0i6am2prtOLJTdu4NJQdcLlSfSUMaUQ,2590
|
|
5
5
|
sqlspec/_sql.py,sha256=j9WljOgCme4jTfL6NegEWOhK-Rr3JEmhtbneh8ZN1bQ,45221
|
|
6
6
|
sqlspec/_typing.py,sha256=jv-7QHGLrJLfnP86bR-Xcmj3PDoddNZEKDz_vYRBiAU,22684
|
|
7
|
-
sqlspec/base.py,sha256=
|
|
7
|
+
sqlspec/base.py,sha256=koDh1AecwCAkntSqSda6J_cpMOLonXiV6hh3GCCXf_s,25459
|
|
8
8
|
sqlspec/cli.py,sha256=Fe5Wbnrb_fkE9qm4gbBEXx3d0Q7VR-S-1t76ouAx2mg,20120
|
|
9
9
|
sqlspec/config.py,sha256=PQKKLXst_uMvqvTSQib6qMZfJd-g3Kqqlp7XLn9kA8A,21640
|
|
10
10
|
sqlspec/exceptions.py,sha256=zBnzQOfYAgqX04GoaC9Io6ardzinldkEuZ3YtR5vr9U,6071
|
|
11
|
-
sqlspec/loader.py,sha256=
|
|
12
|
-
sqlspec/protocols.py,sha256=
|
|
11
|
+
sqlspec/loader.py,sha256=4Gl4LcdVwEB6-4F-KiCnlI3nzyG3LVOFvRMAQNC64qE,23748
|
|
12
|
+
sqlspec/protocols.py,sha256=jSO2OeZvywqkaIvLRFDkQajJvDlErCICipToUH3Mvoo,12996
|
|
13
13
|
sqlspec/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
14
|
sqlspec/typing.py,sha256=yj8D8O-pkfUVZDfVHEgQaB95-5alwgQbp_sqNJOVhvQ,6301
|
|
15
15
|
sqlspec/adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -93,7 +93,7 @@ sqlspec/driver/_async.py,sha256=aS5AwY5IYqnVT8ldDLDwz2AMDN94CI9hfsOz-1k-Nus,1899
|
|
|
93
93
|
sqlspec/driver/_common.py,sha256=Fi5NCy5_OVlRKDzUpGsLJn3zDmrsVSsXggMRndIMM1E,23879
|
|
94
94
|
sqlspec/driver/_sync.py,sha256=wCBV9QfAH8BPjrrVCQc2eM90ai5-FYbKDd81L5sZMS0,18767
|
|
95
95
|
sqlspec/driver/mixins/__init__.py,sha256=gN4pQyJXxNy0xi91dcMJGA7DQ7TbjGjQI24SSpZc6Go,248
|
|
96
|
-
sqlspec/driver/mixins/_result_tools.py,sha256=
|
|
96
|
+
sqlspec/driver/mixins/_result_tools.py,sha256=0LquMpoLBJrqkxaAPU4Wvn2JElyqdwRAnRlwGTX-D7w,8603
|
|
97
97
|
sqlspec/driver/mixins/_sql_translator.py,sha256=TACtUUJdx8tJwuq_7g3AR_k0bKokvuJrMEwINyWwdQM,3711
|
|
98
98
|
sqlspec/extensions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
99
99
|
sqlspec/extensions/aiosql/__init__.py,sha256=-9cefc9pYPf9vCgALoB-y1DtmcgRjKe2azfl6RIarAA,414
|
|
@@ -112,27 +112,28 @@ sqlspec/migrations/loaders.py,sha256=wildbpkyHrE--HXspChPOajSHSBUrfG0e6xQ2buze_4
|
|
|
112
112
|
sqlspec/migrations/runner.py,sha256=y6fyZi02n8MseKR8XFWXUEOOYQNG_w_DikHVxH9p20M,10730
|
|
113
113
|
sqlspec/migrations/tracker.py,sha256=hfrZGz8M70SfFniw4aXVtHNg4p8EPFm67vthjfUMUys,6843
|
|
114
114
|
sqlspec/migrations/utils.py,sha256=Ft5mS1GFiRPLhfUTfPU4ZnEgOkXDnmEHjeeWwfZDcv4,3737
|
|
115
|
-
sqlspec/storage/__init__.py,sha256=
|
|
116
|
-
sqlspec/storage/
|
|
117
|
-
sqlspec/storage/
|
|
118
|
-
sqlspec/storage/backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
115
|
+
sqlspec/storage/__init__.py,sha256=IX7xLuymGdQKOBQL1sNEA-bzZRJGjncUbQbs6ZToDDE,395
|
|
116
|
+
sqlspec/storage/registry.py,sha256=yJZrLDu1vr86C6En3MlzCPOjUNYkvA5Ow-AKrLEDvpI,10754
|
|
117
|
+
sqlspec/storage/backends/__init__.py,sha256=3oSqwEQ_trU2QoxtUTX-5IFeOvo7WGcESY6gIfcmSaE,24
|
|
119
118
|
sqlspec/storage/backends/base.py,sha256=KS2JRZILoH_R_xsfKtYkqQ5a1r5OOBDSE5KbibTmhGY,5730
|
|
120
|
-
sqlspec/storage/backends/fsspec.py,sha256=
|
|
121
|
-
sqlspec/storage/backends/
|
|
119
|
+
sqlspec/storage/backends/fsspec.py,sha256=5X3QhoaAddzMqkeTHs8M5RINj3mrHcnaIVWb8VhC4lQ,13582
|
|
120
|
+
sqlspec/storage/backends/local.py,sha256=vsFnttNesNYjsY_l-KoH6rld51L3E6mxeODtMJawFPQ,12793
|
|
121
|
+
sqlspec/storage/backends/obstore.py,sha256=4RUZAMCF16h1IQ4dEib-0j8TLnhPf0Jhy2lmRJSe7Kw,20512
|
|
122
122
|
sqlspec/utils/__init__.py,sha256=cNFX26-bLyZTyTfujUitfDkUy1CeG_d-EIr8kZ0z4W8,474
|
|
123
123
|
sqlspec/utils/correlation.py,sha256=2jvkAY3nkU3UxNU_9pbBR6cz3A1Q1cGG9IaWSSOIb1Q,4195
|
|
124
|
+
sqlspec/utils/data_transformation.py,sha256=U37zyxR4f5PxsxKdC7QzcMyJxfqpsXUxgH_ch5l3PbY,3951
|
|
124
125
|
sqlspec/utils/deprecation.py,sha256=iy7xzws6Kx0oQpX94smyZzTY6ijdODrdSEFFEXZfp5o,3980
|
|
125
126
|
sqlspec/utils/fixtures.py,sha256=qnPAdkV91dyKOqslm_TH5UZ8mx4koQMwriE5YPn3PgI,9425
|
|
126
127
|
sqlspec/utils/logging.py,sha256=zAM7rHJ-KsmAj1yjvU9QFoiwf4Q2hKTere2J62FlllI,3664
|
|
127
128
|
sqlspec/utils/module_loader.py,sha256=rO4ht-fUSJ3Us7L_7fb_G9bdMCoUSABGUA0pc3ouh9Y,2995
|
|
128
129
|
sqlspec/utils/serializers.py,sha256=GXsTkJbWAhRS7xDMk6WBouZwPeG4sI_brLdMBlIetNg,318
|
|
129
130
|
sqlspec/utils/singleton.py,sha256=-j-s6LS0pP_wTEUYIyK2wSdoeIE_tn7O7B-j7_aODRQ,1252
|
|
130
|
-
sqlspec/utils/sync_tools.py,sha256=
|
|
131
|
-
sqlspec/utils/text.py,sha256=
|
|
132
|
-
sqlspec/utils/type_guards.py,sha256=
|
|
133
|
-
sqlspec-0.
|
|
134
|
-
sqlspec-0.
|
|
135
|
-
sqlspec-0.
|
|
136
|
-
sqlspec-0.
|
|
137
|
-
sqlspec-0.
|
|
138
|
-
sqlspec-0.
|
|
131
|
+
sqlspec/utils/sync_tools.py,sha256=ONdhmx1Dq0_c6ReRaTlXzz6dVmAwz6CybCvsTUAVu1g,8768
|
|
132
|
+
sqlspec/utils/text.py,sha256=ZqaXCVuUbdj_110pdTYjmAxfV3ZtR7J6EixuNazQLFY,3333
|
|
133
|
+
sqlspec/utils/type_guards.py,sha256=ktXwBQLLqOvk1W2wJcmk3bUprrsegs8nAZ879qDe0AU,32880
|
|
134
|
+
sqlspec-0.22.0.dist-info/METADATA,sha256=DZJrWaO7T5jLPe2GlL6M_MmKveqCb-oU1Awx3CDm6J0,23548
|
|
135
|
+
sqlspec-0.22.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
136
|
+
sqlspec-0.22.0.dist-info/entry_points.txt,sha256=G-ZqY1Nuuw3Iys7nXw23f6ILenk_Lt47VdK2mhJCWHg,53
|
|
137
|
+
sqlspec-0.22.0.dist-info/licenses/LICENSE,sha256=MdujfZ6l5HuLz4mElxlu049itenOR3gnhN1_Nd3nVcM,1078
|
|
138
|
+
sqlspec-0.22.0.dist-info/licenses/NOTICE,sha256=Lyir8ozXWov7CyYS4huVaOCNrtgL17P-bNV-5daLntQ,1634
|
|
139
|
+
sqlspec-0.22.0.dist-info/RECORD,,
|