sqlspec 0.14.1__py3-none-any.whl → 0.16.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 +50 -25
- sqlspec/__main__.py +1 -1
- sqlspec/__metadata__.py +1 -3
- sqlspec/_serialization.py +1 -2
- sqlspec/_sql.py +480 -121
- sqlspec/_typing.py +278 -142
- sqlspec/adapters/adbc/__init__.py +4 -3
- sqlspec/adapters/adbc/_types.py +12 -0
- sqlspec/adapters/adbc/config.py +115 -260
- sqlspec/adapters/adbc/driver.py +462 -367
- sqlspec/adapters/aiosqlite/__init__.py +18 -3
- sqlspec/adapters/aiosqlite/_types.py +13 -0
- sqlspec/adapters/aiosqlite/config.py +199 -129
- sqlspec/adapters/aiosqlite/driver.py +230 -269
- sqlspec/adapters/asyncmy/__init__.py +18 -3
- sqlspec/adapters/asyncmy/_types.py +12 -0
- sqlspec/adapters/asyncmy/config.py +80 -168
- sqlspec/adapters/asyncmy/driver.py +260 -225
- sqlspec/adapters/asyncpg/__init__.py +19 -4
- sqlspec/adapters/asyncpg/_types.py +17 -0
- sqlspec/adapters/asyncpg/config.py +82 -181
- sqlspec/adapters/asyncpg/driver.py +285 -383
- sqlspec/adapters/bigquery/__init__.py +17 -3
- sqlspec/adapters/bigquery/_types.py +12 -0
- sqlspec/adapters/bigquery/config.py +191 -258
- sqlspec/adapters/bigquery/driver.py +474 -646
- sqlspec/adapters/duckdb/__init__.py +14 -3
- sqlspec/adapters/duckdb/_types.py +12 -0
- sqlspec/adapters/duckdb/config.py +415 -351
- sqlspec/adapters/duckdb/driver.py +343 -413
- sqlspec/adapters/oracledb/__init__.py +19 -5
- sqlspec/adapters/oracledb/_types.py +14 -0
- sqlspec/adapters/oracledb/config.py +123 -379
- sqlspec/adapters/oracledb/driver.py +507 -560
- sqlspec/adapters/psqlpy/__init__.py +13 -3
- sqlspec/adapters/psqlpy/_types.py +11 -0
- sqlspec/adapters/psqlpy/config.py +93 -254
- sqlspec/adapters/psqlpy/driver.py +505 -234
- sqlspec/adapters/psycopg/__init__.py +19 -5
- sqlspec/adapters/psycopg/_types.py +17 -0
- sqlspec/adapters/psycopg/config.py +143 -403
- sqlspec/adapters/psycopg/driver.py +706 -872
- sqlspec/adapters/sqlite/__init__.py +14 -3
- sqlspec/adapters/sqlite/_types.py +11 -0
- sqlspec/adapters/sqlite/config.py +202 -118
- sqlspec/adapters/sqlite/driver.py +264 -303
- sqlspec/base.py +105 -9
- sqlspec/{statement/builder → builder}/__init__.py +12 -14
- sqlspec/{statement/builder → builder}/_base.py +120 -55
- sqlspec/{statement/builder → builder}/_column.py +17 -6
- sqlspec/{statement/builder → builder}/_ddl.py +46 -79
- sqlspec/{statement/builder → builder}/_ddl_utils.py +5 -10
- sqlspec/{statement/builder → builder}/_delete.py +6 -25
- sqlspec/{statement/builder → builder}/_insert.py +18 -65
- sqlspec/builder/_merge.py +56 -0
- sqlspec/{statement/builder → builder}/_parsing_utils.py +8 -11
- sqlspec/{statement/builder → builder}/_select.py +11 -56
- sqlspec/{statement/builder → builder}/_update.py +12 -18
- sqlspec/{statement/builder → builder}/mixins/__init__.py +10 -14
- sqlspec/{statement/builder → builder}/mixins/_cte_and_set_ops.py +48 -59
- sqlspec/{statement/builder → builder}/mixins/_insert_operations.py +34 -18
- sqlspec/{statement/builder → builder}/mixins/_join_operations.py +1 -3
- sqlspec/{statement/builder → builder}/mixins/_merge_operations.py +19 -9
- sqlspec/{statement/builder → builder}/mixins/_order_limit_operations.py +3 -3
- sqlspec/{statement/builder → builder}/mixins/_pivot_operations.py +4 -8
- sqlspec/{statement/builder → builder}/mixins/_select_operations.py +25 -38
- sqlspec/{statement/builder → builder}/mixins/_update_operations.py +15 -16
- sqlspec/{statement/builder → builder}/mixins/_where_clause.py +210 -137
- sqlspec/cli.py +4 -5
- sqlspec/config.py +180 -133
- sqlspec/core/__init__.py +63 -0
- sqlspec/core/cache.py +873 -0
- sqlspec/core/compiler.py +396 -0
- sqlspec/core/filters.py +830 -0
- sqlspec/core/hashing.py +310 -0
- sqlspec/core/parameters.py +1209 -0
- sqlspec/core/result.py +664 -0
- sqlspec/{statement → core}/splitter.py +321 -191
- sqlspec/core/statement.py +666 -0
- sqlspec/driver/__init__.py +7 -10
- sqlspec/driver/_async.py +387 -176
- sqlspec/driver/_common.py +527 -289
- sqlspec/driver/_sync.py +390 -172
- sqlspec/driver/mixins/__init__.py +2 -19
- sqlspec/driver/mixins/_result_tools.py +164 -0
- sqlspec/driver/mixins/_sql_translator.py +6 -3
- sqlspec/exceptions.py +5 -252
- sqlspec/extensions/aiosql/adapter.py +93 -96
- sqlspec/extensions/litestar/cli.py +1 -1
- sqlspec/extensions/litestar/config.py +0 -1
- sqlspec/extensions/litestar/handlers.py +15 -26
- sqlspec/extensions/litestar/plugin.py +18 -16
- sqlspec/extensions/litestar/providers.py +17 -52
- sqlspec/loader.py +424 -105
- sqlspec/migrations/__init__.py +12 -0
- sqlspec/migrations/base.py +92 -68
- sqlspec/migrations/commands.py +24 -106
- sqlspec/migrations/loaders.py +402 -0
- sqlspec/migrations/runner.py +49 -51
- sqlspec/migrations/tracker.py +31 -44
- sqlspec/migrations/utils.py +64 -24
- sqlspec/protocols.py +7 -183
- sqlspec/storage/__init__.py +1 -1
- sqlspec/storage/backends/base.py +37 -40
- sqlspec/storage/backends/fsspec.py +136 -112
- sqlspec/storage/backends/obstore.py +138 -160
- sqlspec/storage/capabilities.py +5 -4
- sqlspec/storage/registry.py +57 -106
- sqlspec/typing.py +136 -115
- sqlspec/utils/__init__.py +2 -3
- sqlspec/utils/correlation.py +0 -3
- sqlspec/utils/deprecation.py +6 -6
- sqlspec/utils/fixtures.py +6 -6
- sqlspec/utils/logging.py +0 -2
- sqlspec/utils/module_loader.py +7 -12
- sqlspec/utils/singleton.py +0 -1
- sqlspec/utils/sync_tools.py +17 -38
- sqlspec/utils/text.py +12 -51
- sqlspec/utils/type_guards.py +443 -232
- {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/METADATA +7 -2
- sqlspec-0.16.0.dist-info/RECORD +134 -0
- sqlspec/adapters/adbc/transformers.py +0 -108
- sqlspec/driver/connection.py +0 -207
- sqlspec/driver/mixins/_cache.py +0 -114
- sqlspec/driver/mixins/_csv_writer.py +0 -91
- sqlspec/driver/mixins/_pipeline.py +0 -508
- sqlspec/driver/mixins/_query_tools.py +0 -796
- sqlspec/driver/mixins/_result_utils.py +0 -138
- sqlspec/driver/mixins/_storage.py +0 -912
- sqlspec/driver/mixins/_type_coercion.py +0 -128
- sqlspec/driver/parameters.py +0 -138
- sqlspec/statement/__init__.py +0 -21
- sqlspec/statement/builder/_merge.py +0 -95
- sqlspec/statement/cache.py +0 -50
- sqlspec/statement/filters.py +0 -625
- sqlspec/statement/parameters.py +0 -956
- sqlspec/statement/pipelines/__init__.py +0 -210
- sqlspec/statement/pipelines/analyzers/__init__.py +0 -9
- sqlspec/statement/pipelines/analyzers/_analyzer.py +0 -646
- sqlspec/statement/pipelines/context.py +0 -109
- sqlspec/statement/pipelines/transformers/__init__.py +0 -7
- sqlspec/statement/pipelines/transformers/_expression_simplifier.py +0 -88
- sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +0 -1247
- sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +0 -76
- sqlspec/statement/pipelines/validators/__init__.py +0 -23
- sqlspec/statement/pipelines/validators/_dml_safety.py +0 -290
- sqlspec/statement/pipelines/validators/_parameter_style.py +0 -370
- sqlspec/statement/pipelines/validators/_performance.py +0 -714
- sqlspec/statement/pipelines/validators/_security.py +0 -967
- sqlspec/statement/result.py +0 -435
- sqlspec/statement/sql.py +0 -1774
- sqlspec/utils/cached_property.py +0 -25
- sqlspec/utils/statement_hashing.py +0 -203
- sqlspec-0.14.1.dist-info/RECORD +0 -145
- /sqlspec/{statement/builder → builder}/mixins/_delete_operations.py +0 -0
- {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/licenses/NOTICE +0 -0
sqlspec/utils/deprecation.py
CHANGED
|
@@ -23,12 +23,12 @@ def warn_deprecation(
|
|
|
23
23
|
info: Optional[str] = None,
|
|
24
24
|
pending: bool = False,
|
|
25
25
|
) -> None:
|
|
26
|
-
"""Warn about a call to a
|
|
26
|
+
"""Warn about a call to a deprecated function.
|
|
27
27
|
|
|
28
28
|
Args:
|
|
29
|
-
version:
|
|
29
|
+
version: SQLSpec version where the deprecation will occur
|
|
30
30
|
deprecated_name: Name of the deprecated function
|
|
31
|
-
removal_in:
|
|
31
|
+
removal_in: SQLSpec version where the deprecated function will be removed
|
|
32
32
|
alternative: Name of a function that should be used instead
|
|
33
33
|
info: Additional information
|
|
34
34
|
pending: Use :class:`warnings.PendingDeprecationWarning` instead of :class:`warnings.DeprecationWarning`
|
|
@@ -72,11 +72,11 @@ def deprecated(
|
|
|
72
72
|
pending: bool = False,
|
|
73
73
|
kind: Optional[Literal["function", "method", "classmethod", "property"]] = None,
|
|
74
74
|
) -> Callable[[Callable[P, T]], Callable[P, T]]:
|
|
75
|
-
"""Create a decorator wrapping a function, method or property with a warning
|
|
75
|
+
"""Create a decorator wrapping a function, method or property with a deprecation warning.
|
|
76
76
|
|
|
77
77
|
Args:
|
|
78
|
-
version:
|
|
79
|
-
removal_in:
|
|
78
|
+
version: SQLSpec version where the deprecation will occur
|
|
79
|
+
removal_in: SQLSpec version where the deprecated function will be removed
|
|
80
80
|
alternative: Name of a function that should be used instead
|
|
81
81
|
info: Additional information
|
|
82
82
|
pending: Use :class:`warnings.PendingDeprecationWarning` instead of :class:`warnings.DeprecationWarning`
|
sqlspec/utils/fixtures.py
CHANGED
|
@@ -8,17 +8,17 @@ __all__ = ("open_fixture", "open_fixture_async")
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def open_fixture(fixtures_path: Any, fixture_name: str) -> Any:
|
|
11
|
-
"""
|
|
11
|
+
"""Load and parse a JSON fixture file.
|
|
12
12
|
|
|
13
13
|
Args:
|
|
14
14
|
fixtures_path: The path to look for fixtures (pathlib.Path or anyio.Path)
|
|
15
|
-
fixture_name
|
|
15
|
+
fixture_name: The fixture name to load.
|
|
16
16
|
|
|
17
17
|
Raises:
|
|
18
18
|
FileNotFoundError: Fixtures not found.
|
|
19
19
|
|
|
20
20
|
Returns:
|
|
21
|
-
|
|
21
|
+
The parsed JSON data
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
24
|
fixture = Path(fixtures_path / f"{fixture_name}.json")
|
|
@@ -31,18 +31,18 @@ def open_fixture(fixtures_path: Any, fixture_name: str) -> Any:
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
async def open_fixture_async(fixtures_path: Any, fixture_name: str) -> Any:
|
|
34
|
-
"""
|
|
34
|
+
"""Load and parse a JSON fixture file asynchronously.
|
|
35
35
|
|
|
36
36
|
Args:
|
|
37
37
|
fixtures_path: The path to look for fixtures (pathlib.Path or anyio.Path)
|
|
38
|
-
fixture_name
|
|
38
|
+
fixture_name: The fixture name to load.
|
|
39
39
|
|
|
40
40
|
Raises:
|
|
41
41
|
FileNotFoundError: Fixtures not found.
|
|
42
42
|
MissingDependencyError: The `anyio` library is required to use this function.
|
|
43
43
|
|
|
44
44
|
Returns:
|
|
45
|
-
|
|
45
|
+
The parsed JSON data
|
|
46
46
|
"""
|
|
47
47
|
try:
|
|
48
48
|
from anyio import Path as AsyncPath
|
sqlspec/utils/logging.py
CHANGED
|
@@ -18,7 +18,6 @@ if TYPE_CHECKING:
|
|
|
18
18
|
|
|
19
19
|
__all__ = ("StructuredFormatter", "correlation_id_var", "get_correlation_id", "get_logger", "set_correlation_id")
|
|
20
20
|
|
|
21
|
-
# Context variable for correlation ID tracking
|
|
22
21
|
correlation_id_var: ContextVar[str | None] = ContextVar("correlation_id", default=None)
|
|
23
22
|
|
|
24
23
|
|
|
@@ -52,7 +51,6 @@ class StructuredFormatter(logging.Formatter):
|
|
|
52
51
|
Returns:
|
|
53
52
|
JSON formatted log entry
|
|
54
53
|
"""
|
|
55
|
-
# Base log entry
|
|
56
54
|
log_entry = {
|
|
57
55
|
"timestamp": self.formatTime(record, self.datefmt),
|
|
58
56
|
"level": record.levelname,
|
sqlspec/utils/module_loader.py
CHANGED
|
@@ -9,19 +9,16 @@ __all__ = ("import_string", "module_to_os_path")
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def module_to_os_path(dotted_path: str = "app") -> "Path":
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
Return a path to the base directory of the project or the module
|
|
15
|
-
specified by `dotted_path`.
|
|
12
|
+
"""Convert a module dotted path to filesystem path.
|
|
16
13
|
|
|
17
14
|
Args:
|
|
18
|
-
dotted_path: The path to the module.
|
|
15
|
+
dotted_path: The path to the module.
|
|
19
16
|
|
|
20
17
|
Raises:
|
|
21
18
|
TypeError: The module could not be found.
|
|
22
19
|
|
|
23
20
|
Returns:
|
|
24
|
-
|
|
21
|
+
The path to the module.
|
|
25
22
|
"""
|
|
26
23
|
try:
|
|
27
24
|
if (src := find_spec(dotted_path)) is None: # pragma: no cover
|
|
@@ -36,16 +33,13 @@ def module_to_os_path(dotted_path: str = "app") -> "Path":
|
|
|
36
33
|
|
|
37
34
|
|
|
38
35
|
def import_string(dotted_path: str) -> "Any":
|
|
39
|
-
"""
|
|
40
|
-
|
|
41
|
-
Import a dotted module path and return the attribute/class designated by the
|
|
42
|
-
last name in the path. Raise ImportError if the import failed.
|
|
36
|
+
"""Import a module or attribute from a dotted path string.
|
|
43
37
|
|
|
44
38
|
Args:
|
|
45
39
|
dotted_path: The path of the module to import.
|
|
46
40
|
|
|
47
41
|
Returns:
|
|
48
|
-
|
|
42
|
+
The imported object.
|
|
49
43
|
"""
|
|
50
44
|
|
|
51
45
|
def _raise_import_error(msg: str, exc: "Optional[Exception]" = None) -> None:
|
|
@@ -57,7 +51,7 @@ def import_string(dotted_path: str) -> "Any":
|
|
|
57
51
|
try:
|
|
58
52
|
parts = dotted_path.split(".")
|
|
59
53
|
module = None
|
|
60
|
-
i = len(parts)
|
|
54
|
+
i = len(parts)
|
|
61
55
|
|
|
62
56
|
for i in range(len(parts), 0, -1):
|
|
63
57
|
module_path = ".".join(parts[:i])
|
|
@@ -83,6 +77,7 @@ def import_string(dotted_path: str) -> "Any":
|
|
|
83
77
|
return obj
|
|
84
78
|
if not hasattr(parent_module, attr):
|
|
85
79
|
_raise_import_error(f"Module '{parent_module_path}' has no attribute '{attr}' in '{dotted_path}'")
|
|
80
|
+
|
|
86
81
|
for attr in attrs:
|
|
87
82
|
if not hasattr(obj, attr):
|
|
88
83
|
_raise_import_error(
|
sqlspec/utils/singleton.py
CHANGED
sqlspec/utils/sync_tools.py
CHANGED
|
@@ -14,7 +14,7 @@ if TYPE_CHECKING:
|
|
|
14
14
|
try:
|
|
15
15
|
import uvloop # pyright: ignore[reportMissingImports]
|
|
16
16
|
except ImportError:
|
|
17
|
-
uvloop = None
|
|
17
|
+
uvloop = None # type: ignore[assignment,unused-ignore]
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
ReturnT = TypeVar("ReturnT")
|
|
@@ -61,11 +61,10 @@ def run_(async_function: "Callable[ParamSpecT, Coroutine[Any, Any, ReturnT]]") -
|
|
|
61
61
|
"""Convert an async function to a blocking function using asyncio.run().
|
|
62
62
|
|
|
63
63
|
Args:
|
|
64
|
-
async_function
|
|
64
|
+
async_function: The async function to convert.
|
|
65
65
|
|
|
66
66
|
Returns:
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
A blocking function that runs the async function.
|
|
69
68
|
"""
|
|
70
69
|
|
|
71
70
|
@functools.wraps(async_function)
|
|
@@ -77,7 +76,6 @@ def run_(async_function: "Callable[ParamSpecT, Coroutine[Any, Any, ReturnT]]") -
|
|
|
77
76
|
loop = None
|
|
78
77
|
|
|
79
78
|
if loop is not None:
|
|
80
|
-
# Running in an existing event loop
|
|
81
79
|
return asyncio.run(partial_f())
|
|
82
80
|
if uvloop and sys.platform != "win32":
|
|
83
81
|
uvloop.install() # pyright: ignore[reportUnknownMemberType]
|
|
@@ -92,12 +90,12 @@ def await_(
|
|
|
92
90
|
"""Convert an async function to a blocking one, running in the main async loop.
|
|
93
91
|
|
|
94
92
|
Args:
|
|
95
|
-
async_function
|
|
96
|
-
raise_sync_error
|
|
97
|
-
|
|
93
|
+
async_function: The async function to convert.
|
|
94
|
+
raise_sync_error: If False, runs in a new event loop if no loop is present.
|
|
95
|
+
If True (default), raises RuntimeError if no loop is running.
|
|
98
96
|
|
|
99
97
|
Returns:
|
|
100
|
-
|
|
98
|
+
A blocking function that runs the async function.
|
|
101
99
|
"""
|
|
102
100
|
|
|
103
101
|
@functools.wraps(async_function)
|
|
@@ -111,27 +109,17 @@ def await_(
|
|
|
111
109
|
raise RuntimeError(msg) from None
|
|
112
110
|
return asyncio.run(partial_f())
|
|
113
111
|
else:
|
|
114
|
-
# Running in an existing event loop.
|
|
115
112
|
if loop.is_running():
|
|
116
113
|
try:
|
|
117
114
|
current_task = asyncio.current_task(loop=loop)
|
|
118
115
|
except RuntimeError:
|
|
119
|
-
# Not running inside a task managed by this loop
|
|
120
116
|
current_task = None
|
|
121
117
|
|
|
122
118
|
if current_task is not None:
|
|
123
|
-
# Called from within the event loop's execution context (a task).
|
|
124
|
-
# Blocking here would deadlock the loop.
|
|
125
119
|
msg = "await_ cannot be called from within an async task running on the same event loop. Use 'await' instead."
|
|
126
120
|
raise RuntimeError(msg)
|
|
127
|
-
# Called from a different thread than the loop's thread.
|
|
128
|
-
# It's safe to block this thread and wait for the loop.
|
|
129
121
|
future = asyncio.run_coroutine_threadsafe(partial_f(), loop)
|
|
130
|
-
# This blocks the *calling* thread, not the loop thread.
|
|
131
122
|
return future.result()
|
|
132
|
-
# This case should ideally not happen if get_running_loop() succeeded
|
|
133
|
-
# but the loop isn't running, but handle defensively.
|
|
134
|
-
# loop is not running
|
|
135
123
|
if raise_sync_error:
|
|
136
124
|
msg = "Cannot run async function"
|
|
137
125
|
raise RuntimeError(msg)
|
|
@@ -146,12 +134,11 @@ def async_(
|
|
|
146
134
|
"""Convert a blocking function to an async one using asyncio.to_thread().
|
|
147
135
|
|
|
148
136
|
Args:
|
|
149
|
-
function
|
|
150
|
-
|
|
151
|
-
limiter (CapacityLimiter, optional): Limit the total number of threads.
|
|
137
|
+
function: The blocking function to convert.
|
|
138
|
+
limiter: Limit the total number of threads.
|
|
152
139
|
|
|
153
140
|
Returns:
|
|
154
|
-
|
|
141
|
+
An async function that runs the original function in a thread.
|
|
155
142
|
"""
|
|
156
143
|
|
|
157
144
|
@functools.wraps(function)
|
|
@@ -170,10 +157,10 @@ def ensure_async_(
|
|
|
170
157
|
"""Convert a function to an async one if it is not already.
|
|
171
158
|
|
|
172
159
|
Args:
|
|
173
|
-
function
|
|
160
|
+
function: The function to convert.
|
|
174
161
|
|
|
175
162
|
Returns:
|
|
176
|
-
|
|
163
|
+
An async function that runs the original function.
|
|
177
164
|
"""
|
|
178
165
|
if inspect.iscoroutinefunction(function):
|
|
179
166
|
return function
|
|
@@ -210,10 +197,10 @@ def with_ensure_async_(
|
|
|
210
197
|
"""Convert a context manager to an async one if it is not already.
|
|
211
198
|
|
|
212
199
|
Args:
|
|
213
|
-
obj
|
|
200
|
+
obj: The context manager to convert.
|
|
214
201
|
|
|
215
202
|
Returns:
|
|
216
|
-
|
|
203
|
+
An async context manager that runs the original context manager.
|
|
217
204
|
"""
|
|
218
205
|
|
|
219
206
|
if isinstance(obj, AbstractContextManager):
|
|
@@ -222,30 +209,22 @@ def with_ensure_async_(
|
|
|
222
209
|
|
|
223
210
|
|
|
224
211
|
class NoValue:
|
|
225
|
-
"""
|
|
212
|
+
"""Sentinel class for missing values."""
|
|
226
213
|
|
|
227
214
|
|
|
228
215
|
async def get_next(iterable: Any, default: Any = NoValue, *args: Any) -> Any: # pragma: no cover
|
|
229
216
|
"""Return the next item from an async iterator.
|
|
230
217
|
|
|
231
|
-
In Python <3.10, `anext` is not available. This function is a drop-in replacement.
|
|
232
|
-
|
|
233
|
-
This function will return the next value form an async iterable. If the
|
|
234
|
-
iterable is empty the StopAsyncIteration will be propagated. However, if
|
|
235
|
-
a default value is given as a second argument the exception is silenced and
|
|
236
|
-
the default value is returned instead.
|
|
237
|
-
|
|
238
218
|
Args:
|
|
239
219
|
iterable: An async iterable.
|
|
240
220
|
default: An optional default value to return if the iterable is empty.
|
|
241
221
|
*args: The remaining args
|
|
242
|
-
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
243
224
|
The next value of the iterable.
|
|
244
225
|
|
|
245
226
|
Raises:
|
|
246
227
|
StopAsyncIteration: The iterable given is not async.
|
|
247
|
-
|
|
248
|
-
|
|
249
228
|
"""
|
|
250
229
|
has_default = bool(not isinstance(default, NoValue))
|
|
251
230
|
try:
|
sqlspec/utils/text.py
CHANGED
|
@@ -5,35 +5,29 @@ import unicodedata
|
|
|
5
5
|
from functools import lru_cache
|
|
6
6
|
from typing import Optional
|
|
7
7
|
|
|
8
|
-
# Compiled regex for slugify
|
|
9
8
|
_SLUGIFY_REMOVE_NON_ALPHANUMERIC = re.compile(r"[^\w]+", re.UNICODE)
|
|
10
9
|
_SLUGIFY_HYPHEN_COLLAPSE = re.compile(r"-+")
|
|
11
10
|
|
|
12
|
-
# Compiled regex for snake_case
|
|
13
|
-
# Insert underscore between lowercase/digit and uppercase letter
|
|
14
11
|
_SNAKE_CASE_LOWER_OR_DIGIT_TO_UPPER = re.compile(r"(?<=[a-z0-9])(?=[A-Z])", re.UNICODE)
|
|
15
|
-
# Insert underscore between uppercase letter and uppercase followed by lowercase
|
|
16
12
|
_SNAKE_CASE_UPPER_TO_UPPER_LOWER = re.compile(r"(?<=[A-Z])(?=[A-Z][a-z])", re.UNICODE)
|
|
17
13
|
_SNAKE_CASE_HYPHEN_SPACE = re.compile(r"[.\s@-]+", re.UNICODE)
|
|
18
|
-
|
|
14
|
+
_SNAKE_CASE_REMOVE_NON_WORD = re.compile(r"[^\w]+", re.UNICODE)
|
|
19
15
|
_SNAKE_CASE_MULTIPLE_UNDERSCORES = re.compile(r"__+", re.UNICODE)
|
|
20
16
|
|
|
21
17
|
__all__ = ("camelize", "check_email", "slugify", "snake_case")
|
|
22
18
|
|
|
23
19
|
|
|
24
20
|
def check_email(email: str) -> str:
|
|
25
|
-
"""Validate an email.
|
|
26
|
-
|
|
27
|
-
Very simple email validation.
|
|
21
|
+
"""Validate an email address.
|
|
28
22
|
|
|
29
23
|
Args:
|
|
30
|
-
email
|
|
24
|
+
email: The email to validate.
|
|
31
25
|
|
|
32
26
|
Raises:
|
|
33
27
|
ValueError: If the email is invalid.
|
|
34
28
|
|
|
35
29
|
Returns:
|
|
36
|
-
|
|
30
|
+
The validated email.
|
|
37
31
|
"""
|
|
38
32
|
if "@" not in email:
|
|
39
33
|
msg = "Invalid email!"
|
|
@@ -42,21 +36,15 @@ def check_email(email: str) -> str:
|
|
|
42
36
|
|
|
43
37
|
|
|
44
38
|
def slugify(value: str, allow_unicode: bool = False, separator: Optional[str] = None) -> str:
|
|
45
|
-
"""
|
|
46
|
-
|
|
47
|
-
Convert to ASCII if ``allow_unicode`` is ``False``. Convert spaces or repeated
|
|
48
|
-
dashes to single dashes. Remove characters that aren't alphanumerics,
|
|
49
|
-
underscores, or hyphens. Convert to lowercase. Also strip leading and
|
|
50
|
-
trailing whitespace, dashes, and underscores.
|
|
39
|
+
"""Convert a string to a URL-friendly slug.
|
|
51
40
|
|
|
52
41
|
Args:
|
|
53
|
-
value
|
|
54
|
-
allow_unicode
|
|
55
|
-
separator
|
|
56
|
-
Set this to configure something different.
|
|
42
|
+
value: The string to slugify
|
|
43
|
+
allow_unicode: Allow unicode characters in slug.
|
|
44
|
+
separator: Separator character for word boundaries. Defaults to "-".
|
|
57
45
|
|
|
58
46
|
Returns:
|
|
59
|
-
|
|
47
|
+
A slugified string.
|
|
60
48
|
"""
|
|
61
49
|
if allow_unicode:
|
|
62
50
|
value = unicodedata.normalize("NFKC", value)
|
|
@@ -67,9 +55,7 @@ def slugify(value: str, allow_unicode: bool = False, separator: Optional[str] =
|
|
|
67
55
|
if not sep:
|
|
68
56
|
return _SLUGIFY_REMOVE_NON_ALPHANUMERIC.sub("", value)
|
|
69
57
|
value = _SLUGIFY_REMOVE_NON_ALPHANUMERIC.sub(sep, value)
|
|
70
|
-
# For dynamic separators, we need to use re.sub with escaped separator
|
|
71
58
|
if sep == "-":
|
|
72
|
-
# Use pre-compiled regex for common case
|
|
73
59
|
value = value.strip("-")
|
|
74
60
|
return _SLUGIFY_HYPHEN_COLLAPSE.sub("-", value)
|
|
75
61
|
value = re.sub(rf"^{re.escape(sep)}+|{re.escape(sep)}+$", "", value)
|
|
@@ -81,10 +67,10 @@ def camelize(string: str) -> str:
|
|
|
81
67
|
"""Convert a string to camel case.
|
|
82
68
|
|
|
83
69
|
Args:
|
|
84
|
-
string
|
|
70
|
+
string: The string to convert.
|
|
85
71
|
|
|
86
72
|
Returns:
|
|
87
|
-
|
|
73
|
+
The converted string.
|
|
88
74
|
"""
|
|
89
75
|
return "".join(word if index == 0 else word.capitalize() for index, word in enumerate(string.split("_")))
|
|
90
76
|
|
|
@@ -93,11 +79,6 @@ def camelize(string: str) -> str:
|
|
|
93
79
|
def snake_case(string: str) -> str:
|
|
94
80
|
"""Convert a string to snake_case.
|
|
95
81
|
|
|
96
|
-
Handles CamelCase, PascalCase, strings with spaces, hyphens, or dots
|
|
97
|
-
as separators, and ensures single underscores. It also correctly
|
|
98
|
-
handles acronyms (e.g., "HTTPRequest" becomes "http_request").
|
|
99
|
-
Handles Unicode letters and numbers.
|
|
100
|
-
|
|
101
82
|
Args:
|
|
102
83
|
string: The string to convert.
|
|
103
84
|
|
|
@@ -106,30 +87,10 @@ def snake_case(string: str) -> str:
|
|
|
106
87
|
"""
|
|
107
88
|
if not string:
|
|
108
89
|
return ""
|
|
109
|
-
# 1. Replace hyphens and spaces with underscores
|
|
110
90
|
s = _SNAKE_CASE_HYPHEN_SPACE.sub("_", string)
|
|
111
|
-
|
|
112
|
-
# 2. Remove all non-alphanumeric characters except underscores
|
|
113
|
-
# TODO: move to a compiled regex at the top of the file
|
|
114
|
-
s = re.sub(r"[^\w]+", "", s, flags=re.UNICODE)
|
|
115
|
-
|
|
116
|
-
# 3. Insert an underscore between a lowercase/digit and an uppercase letter.
|
|
117
|
-
# e.g., "helloWorld" -> "hello_World"
|
|
118
|
-
# e.g., "Python3IsGreat" -> "Python3_IsGreat"
|
|
119
|
-
# Uses a positive lookbehind `(?<=[...])` and a positive lookahead `(?=[...])`
|
|
91
|
+
s = _SNAKE_CASE_REMOVE_NON_WORD.sub("", s)
|
|
120
92
|
s = _SNAKE_CASE_LOWER_OR_DIGIT_TO_UPPER.sub("_", s)
|
|
121
|
-
|
|
122
|
-
# 4. Insert an underscore between an uppercase letter and another
|
|
123
|
-
# uppercase letter followed by a lowercase letter.
|
|
124
|
-
# e.g., "HTTPRequest" -> "HTTP_Request"
|
|
125
|
-
# This handles acronyms gracefully.
|
|
126
93
|
s = _SNAKE_CASE_UPPER_TO_UPPER_LOWER.sub("_", s)
|
|
127
|
-
|
|
128
|
-
# 5. Convert the entire string to lowercase.
|
|
129
94
|
s = s.lower()
|
|
130
|
-
|
|
131
|
-
# 6. Remove any leading or trailing underscores that might have been created.
|
|
132
95
|
s = s.strip("_")
|
|
133
|
-
|
|
134
|
-
# 7. Collapse multiple consecutive underscores into a single one.
|
|
135
96
|
return _SNAKE_CASE_MULTIPLE_UNDERSCORES.sub("_", s)
|