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.

Files changed (212) hide show
  1. sqlspec/__init__.py +7 -15
  2. sqlspec/_serialization.py +55 -25
  3. sqlspec/_typing.py +155 -52
  4. sqlspec/adapters/adbc/_types.py +1 -1
  5. sqlspec/adapters/adbc/adk/__init__.py +5 -0
  6. sqlspec/adapters/adbc/adk/store.py +880 -0
  7. sqlspec/adapters/adbc/config.py +62 -12
  8. sqlspec/adapters/adbc/data_dictionary.py +74 -2
  9. sqlspec/adapters/adbc/driver.py +226 -58
  10. sqlspec/adapters/adbc/litestar/__init__.py +5 -0
  11. sqlspec/adapters/adbc/litestar/store.py +504 -0
  12. sqlspec/adapters/adbc/type_converter.py +44 -50
  13. sqlspec/adapters/aiosqlite/_types.py +1 -1
  14. sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
  15. sqlspec/adapters/aiosqlite/adk/store.py +536 -0
  16. sqlspec/adapters/aiosqlite/config.py +86 -16
  17. sqlspec/adapters/aiosqlite/data_dictionary.py +34 -2
  18. sqlspec/adapters/aiosqlite/driver.py +127 -38
  19. sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
  20. sqlspec/adapters/aiosqlite/litestar/store.py +281 -0
  21. sqlspec/adapters/aiosqlite/pool.py +7 -7
  22. sqlspec/adapters/asyncmy/__init__.py +7 -1
  23. sqlspec/adapters/asyncmy/_types.py +1 -1
  24. sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
  25. sqlspec/adapters/asyncmy/adk/store.py +503 -0
  26. sqlspec/adapters/asyncmy/config.py +59 -17
  27. sqlspec/adapters/asyncmy/data_dictionary.py +41 -2
  28. sqlspec/adapters/asyncmy/driver.py +293 -62
  29. sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
  30. sqlspec/adapters/asyncmy/litestar/store.py +296 -0
  31. sqlspec/adapters/asyncpg/__init__.py +2 -1
  32. sqlspec/adapters/asyncpg/_type_handlers.py +71 -0
  33. sqlspec/adapters/asyncpg/_types.py +11 -7
  34. sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
  35. sqlspec/adapters/asyncpg/adk/store.py +460 -0
  36. sqlspec/adapters/asyncpg/config.py +57 -36
  37. sqlspec/adapters/asyncpg/data_dictionary.py +48 -2
  38. sqlspec/adapters/asyncpg/driver.py +153 -23
  39. sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
  40. sqlspec/adapters/asyncpg/litestar/store.py +253 -0
  41. sqlspec/adapters/bigquery/_types.py +1 -1
  42. sqlspec/adapters/bigquery/adk/__init__.py +5 -0
  43. sqlspec/adapters/bigquery/adk/store.py +585 -0
  44. sqlspec/adapters/bigquery/config.py +36 -11
  45. sqlspec/adapters/bigquery/data_dictionary.py +42 -2
  46. sqlspec/adapters/bigquery/driver.py +489 -144
  47. sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
  48. sqlspec/adapters/bigquery/litestar/store.py +327 -0
  49. sqlspec/adapters/bigquery/type_converter.py +55 -23
  50. sqlspec/adapters/duckdb/_types.py +2 -2
  51. sqlspec/adapters/duckdb/adk/__init__.py +14 -0
  52. sqlspec/adapters/duckdb/adk/store.py +563 -0
  53. sqlspec/adapters/duckdb/config.py +79 -21
  54. sqlspec/adapters/duckdb/data_dictionary.py +41 -2
  55. sqlspec/adapters/duckdb/driver.py +225 -44
  56. sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
  57. sqlspec/adapters/duckdb/litestar/store.py +332 -0
  58. sqlspec/adapters/duckdb/pool.py +5 -5
  59. sqlspec/adapters/duckdb/type_converter.py +51 -21
  60. sqlspec/adapters/oracledb/_numpy_handlers.py +133 -0
  61. sqlspec/adapters/oracledb/_types.py +20 -2
  62. sqlspec/adapters/oracledb/adk/__init__.py +5 -0
  63. sqlspec/adapters/oracledb/adk/store.py +1628 -0
  64. sqlspec/adapters/oracledb/config.py +120 -36
  65. sqlspec/adapters/oracledb/data_dictionary.py +87 -20
  66. sqlspec/adapters/oracledb/driver.py +475 -86
  67. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  68. sqlspec/adapters/oracledb/litestar/store.py +765 -0
  69. sqlspec/adapters/oracledb/migrations.py +316 -25
  70. sqlspec/adapters/oracledb/type_converter.py +91 -16
  71. sqlspec/adapters/psqlpy/_type_handlers.py +44 -0
  72. sqlspec/adapters/psqlpy/_types.py +2 -1
  73. sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
  74. sqlspec/adapters/psqlpy/adk/store.py +483 -0
  75. sqlspec/adapters/psqlpy/config.py +45 -19
  76. sqlspec/adapters/psqlpy/data_dictionary.py +48 -2
  77. sqlspec/adapters/psqlpy/driver.py +108 -41
  78. sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
  79. sqlspec/adapters/psqlpy/litestar/store.py +272 -0
  80. sqlspec/adapters/psqlpy/type_converter.py +40 -11
  81. sqlspec/adapters/psycopg/_type_handlers.py +80 -0
  82. sqlspec/adapters/psycopg/_types.py +2 -1
  83. sqlspec/adapters/psycopg/adk/__init__.py +5 -0
  84. sqlspec/adapters/psycopg/adk/store.py +962 -0
  85. sqlspec/adapters/psycopg/config.py +65 -37
  86. sqlspec/adapters/psycopg/data_dictionary.py +91 -3
  87. sqlspec/adapters/psycopg/driver.py +200 -78
  88. sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
  89. sqlspec/adapters/psycopg/litestar/store.py +554 -0
  90. sqlspec/adapters/sqlite/__init__.py +2 -1
  91. sqlspec/adapters/sqlite/_type_handlers.py +86 -0
  92. sqlspec/adapters/sqlite/_types.py +1 -1
  93. sqlspec/adapters/sqlite/adk/__init__.py +5 -0
  94. sqlspec/adapters/sqlite/adk/store.py +582 -0
  95. sqlspec/adapters/sqlite/config.py +85 -16
  96. sqlspec/adapters/sqlite/data_dictionary.py +34 -2
  97. sqlspec/adapters/sqlite/driver.py +120 -52
  98. sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
  99. sqlspec/adapters/sqlite/litestar/store.py +318 -0
  100. sqlspec/adapters/sqlite/pool.py +5 -5
  101. sqlspec/base.py +45 -26
  102. sqlspec/builder/__init__.py +73 -4
  103. sqlspec/builder/_base.py +91 -58
  104. sqlspec/builder/_column.py +5 -5
  105. sqlspec/builder/_ddl.py +98 -89
  106. sqlspec/builder/_delete.py +5 -4
  107. sqlspec/builder/_dml.py +388 -0
  108. sqlspec/{_sql.py → builder/_factory.py} +41 -44
  109. sqlspec/builder/_insert.py +5 -82
  110. sqlspec/builder/{mixins/_join_operations.py → _join.py} +145 -143
  111. sqlspec/builder/_merge.py +446 -11
  112. sqlspec/builder/_parsing_utils.py +9 -11
  113. sqlspec/builder/_select.py +1313 -25
  114. sqlspec/builder/_update.py +11 -42
  115. sqlspec/cli.py +76 -69
  116. sqlspec/config.py +331 -62
  117. sqlspec/core/__init__.py +5 -4
  118. sqlspec/core/cache.py +18 -18
  119. sqlspec/core/compiler.py +6 -8
  120. sqlspec/core/filters.py +55 -47
  121. sqlspec/core/hashing.py +9 -9
  122. sqlspec/core/parameters.py +76 -45
  123. sqlspec/core/result.py +234 -47
  124. sqlspec/core/splitter.py +16 -17
  125. sqlspec/core/statement.py +32 -31
  126. sqlspec/core/type_conversion.py +3 -2
  127. sqlspec/driver/__init__.py +1 -3
  128. sqlspec/driver/_async.py +183 -160
  129. sqlspec/driver/_common.py +197 -109
  130. sqlspec/driver/_sync.py +189 -161
  131. sqlspec/driver/mixins/_result_tools.py +20 -236
  132. sqlspec/driver/mixins/_sql_translator.py +4 -4
  133. sqlspec/exceptions.py +70 -7
  134. sqlspec/extensions/adk/__init__.py +53 -0
  135. sqlspec/extensions/adk/_types.py +51 -0
  136. sqlspec/extensions/adk/converters.py +172 -0
  137. sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +144 -0
  138. sqlspec/extensions/adk/migrations/__init__.py +0 -0
  139. sqlspec/extensions/adk/service.py +181 -0
  140. sqlspec/extensions/adk/store.py +536 -0
  141. sqlspec/extensions/aiosql/adapter.py +69 -61
  142. sqlspec/extensions/fastapi/__init__.py +21 -0
  143. sqlspec/extensions/fastapi/extension.py +331 -0
  144. sqlspec/extensions/fastapi/providers.py +543 -0
  145. sqlspec/extensions/flask/__init__.py +36 -0
  146. sqlspec/extensions/flask/_state.py +71 -0
  147. sqlspec/extensions/flask/_utils.py +40 -0
  148. sqlspec/extensions/flask/extension.py +389 -0
  149. sqlspec/extensions/litestar/__init__.py +21 -4
  150. sqlspec/extensions/litestar/cli.py +54 -10
  151. sqlspec/extensions/litestar/config.py +56 -266
  152. sqlspec/extensions/litestar/handlers.py +46 -17
  153. sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
  154. sqlspec/extensions/litestar/migrations/__init__.py +3 -0
  155. sqlspec/extensions/litestar/plugin.py +349 -224
  156. sqlspec/extensions/litestar/providers.py +25 -25
  157. sqlspec/extensions/litestar/store.py +265 -0
  158. sqlspec/extensions/starlette/__init__.py +10 -0
  159. sqlspec/extensions/starlette/_state.py +25 -0
  160. sqlspec/extensions/starlette/_utils.py +52 -0
  161. sqlspec/extensions/starlette/extension.py +254 -0
  162. sqlspec/extensions/starlette/middleware.py +154 -0
  163. sqlspec/loader.py +30 -49
  164. sqlspec/migrations/base.py +200 -76
  165. sqlspec/migrations/commands.py +591 -62
  166. sqlspec/migrations/context.py +6 -9
  167. sqlspec/migrations/fix.py +199 -0
  168. sqlspec/migrations/loaders.py +47 -19
  169. sqlspec/migrations/runner.py +241 -75
  170. sqlspec/migrations/tracker.py +237 -21
  171. sqlspec/migrations/utils.py +51 -3
  172. sqlspec/migrations/validation.py +177 -0
  173. sqlspec/protocols.py +106 -36
  174. sqlspec/storage/_utils.py +85 -0
  175. sqlspec/storage/backends/fsspec.py +133 -107
  176. sqlspec/storage/backends/local.py +78 -51
  177. sqlspec/storage/backends/obstore.py +276 -168
  178. sqlspec/storage/registry.py +75 -39
  179. sqlspec/typing.py +30 -84
  180. sqlspec/utils/__init__.py +25 -4
  181. sqlspec/utils/arrow_helpers.py +81 -0
  182. sqlspec/utils/config_resolver.py +6 -6
  183. sqlspec/utils/correlation.py +4 -5
  184. sqlspec/utils/data_transformation.py +3 -2
  185. sqlspec/utils/deprecation.py +9 -8
  186. sqlspec/utils/fixtures.py +4 -4
  187. sqlspec/utils/logging.py +46 -6
  188. sqlspec/utils/module_loader.py +205 -5
  189. sqlspec/utils/portal.py +311 -0
  190. sqlspec/utils/schema.py +288 -0
  191. sqlspec/utils/serializers.py +113 -4
  192. sqlspec/utils/sync_tools.py +36 -22
  193. sqlspec/utils/text.py +1 -2
  194. sqlspec/utils/type_guards.py +136 -20
  195. sqlspec/utils/version.py +433 -0
  196. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/METADATA +41 -22
  197. sqlspec-0.28.0.dist-info/RECORD +221 -0
  198. sqlspec/builder/mixins/__init__.py +0 -55
  199. sqlspec/builder/mixins/_cte_and_set_ops.py +0 -253
  200. sqlspec/builder/mixins/_delete_operations.py +0 -50
  201. sqlspec/builder/mixins/_insert_operations.py +0 -282
  202. sqlspec/builder/mixins/_merge_operations.py +0 -698
  203. sqlspec/builder/mixins/_order_limit_operations.py +0 -145
  204. sqlspec/builder/mixins/_pivot_operations.py +0 -157
  205. sqlspec/builder/mixins/_select_operations.py +0 -930
  206. sqlspec/builder/mixins/_update_operations.py +0 -199
  207. sqlspec/builder/mixins/_where_clause.py +0 -1298
  208. sqlspec-0.26.0.dist-info/RECORD +0 -157
  209. sqlspec-0.26.0.dist-info/licenses/NOTICE +0 -29
  210. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/WHEEL +0 -0
  211. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/entry_points.txt +0 -0
  212. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/licenses/LICENSE +0 -0
@@ -5,8 +5,9 @@ Used to communicate API changes and migration paths to users.
5
5
  """
6
6
 
7
7
  import inspect
8
+ from collections.abc import Callable
8
9
  from functools import wraps
9
- from typing import Callable, Literal, Optional
10
+ from typing import Literal
10
11
  from warnings import warn
11
12
 
12
13
  from typing_extensions import ParamSpec, TypeVar
@@ -24,9 +25,9 @@ def warn_deprecation(
24
25
  deprecated_name: str,
25
26
  kind: DeprecatedKind,
26
27
  *,
27
- removal_in: Optional[str] = None,
28
- alternative: Optional[str] = None,
29
- info: Optional[str] = None,
28
+ removal_in: str | None = None,
29
+ alternative: str | None = None,
30
+ info: str | None = None,
30
31
  pending: bool = False,
31
32
  ) -> None:
32
33
  """Warn about a call to a deprecated function.
@@ -72,11 +73,11 @@ def warn_deprecation(
72
73
  def deprecated(
73
74
  version: str,
74
75
  *,
75
- removal_in: Optional[str] = None,
76
- alternative: Optional[str] = None,
77
- info: Optional[str] = None,
76
+ removal_in: str | None = None,
77
+ alternative: str | None = None,
78
+ info: str | None = None,
78
79
  pending: bool = False,
79
- kind: Optional[Literal["function", "method", "classmethod", "property"]] = None,
80
+ kind: Literal["function", "method", "classmethod", "property"] | None = None,
80
81
  ) -> Callable[[Callable[P, T]], Callable[P, T]]:
81
82
  """Create a decorator wrapping a function, method or property with a deprecation warning.
82
83
 
sqlspec/utils/fixtures.py CHANGED
@@ -7,7 +7,7 @@ used in testing and development. Supports both sync and async operations.
7
7
  import gzip
8
8
  import zipfile
9
9
  from pathlib import Path
10
- from typing import TYPE_CHECKING, Any, Union
10
+ from typing import TYPE_CHECKING, Any
11
11
 
12
12
  from sqlspec.storage import storage_registry
13
13
  from sqlspec.utils.serializers import from_json as decode_json
@@ -16,7 +16,7 @@ from sqlspec.utils.sync_tools import async_
16
16
  from sqlspec.utils.type_guards import schema_dump
17
17
 
18
18
  if TYPE_CHECKING:
19
- from sqlspec.typing import ModelDictList, SupportedSchemaModel
19
+ from sqlspec.typing import SupportedSchemaModel
20
20
 
21
21
  __all__ = ("open_fixture", "open_fixture_async", "write_fixture", "write_fixture_async")
22
22
 
@@ -171,7 +171,7 @@ def _serialize_data(data: Any) -> str:
171
171
  def write_fixture(
172
172
  fixtures_path: str,
173
173
  table_name: str,
174
- data: "Union[ModelDictList, list[dict[str, Any]], SupportedSchemaModel]",
174
+ data: "list[SupportedSchemaModel] | list[dict[str, Any]] | SupportedSchemaModel",
175
175
  storage_backend: str = "local",
176
176
  compress: bool = False,
177
177
  **storage_kwargs: Any,
@@ -219,7 +219,7 @@ def write_fixture(
219
219
  async def write_fixture_async(
220
220
  fixtures_path: str,
221
221
  table_name: str,
222
- data: "Union[ModelDictList, list[dict[str, Any]], SupportedSchemaModel]",
222
+ data: "list[SupportedSchemaModel] | list[dict[str, Any]] | SupportedSchemaModel",
223
223
  storage_backend: str = "local",
224
224
  compress: bool = False,
225
225
  **storage_kwargs: Any,
sqlspec/utils/logging.py CHANGED
@@ -8,16 +8,24 @@ SQLSpec provides StructuredFormatter for JSON-formatted logs if desired.
8
8
  import logging
9
9
  from contextvars import ContextVar
10
10
  from logging import LogRecord
11
- from typing import Any, Optional
11
+ from typing import Any
12
12
 
13
13
  from sqlspec._serialization import encode_json
14
14
 
15
- __all__ = ("StructuredFormatter", "correlation_id_var", "get_correlation_id", "get_logger", "set_correlation_id")
15
+ __all__ = (
16
+ "SqlglotCommandFallbackFilter",
17
+ "StructuredFormatter",
18
+ "correlation_id_var",
19
+ "get_correlation_id",
20
+ "get_logger",
21
+ "set_correlation_id",
22
+ "suppress_erroneous_sqlglot_log_messages",
23
+ )
16
24
 
17
- correlation_id_var: "ContextVar[Optional[str]]" = ContextVar("correlation_id", default=None)
25
+ correlation_id_var: "ContextVar[str | None]" = ContextVar("correlation_id", default=None)
18
26
 
19
27
 
20
- def set_correlation_id(correlation_id: "Optional[str]") -> None:
28
+ def set_correlation_id(correlation_id: "str | None") -> None:
21
29
  """Set the correlation ID for the current context.
22
30
 
23
31
  Args:
@@ -26,7 +34,7 @@ def set_correlation_id(correlation_id: "Optional[str]") -> None:
26
34
  correlation_id_var.set(correlation_id)
27
35
 
28
36
 
29
- def get_correlation_id() -> "Optional[str]":
37
+ def get_correlation_id() -> "str | None":
30
38
  """Get the current correlation ID.
31
39
 
32
40
  Returns:
@@ -86,7 +94,27 @@ class CorrelationIDFilter(logging.Filter):
86
94
  return True
87
95
 
88
96
 
89
- def get_logger(name: "Optional[str]" = None) -> logging.Logger:
97
+ class SqlglotCommandFallbackFilter(logging.Filter):
98
+ """Filter to suppress sqlglot's confusing 'Falling back to Command' warning.
99
+
100
+ This filter suppresses the warning message that sqlglot emits when it
101
+ encounters unsupported syntax and falls back to parsing as a Command.
102
+ This is expected behavior in SQLSpec and the warning is confusing to users.
103
+ """
104
+
105
+ def filter(self, record: LogRecord) -> bool:
106
+ """Suppress the 'Falling back to Command' warning message.
107
+
108
+ Args:
109
+ record: The log record to evaluate
110
+
111
+ Returns:
112
+ False if the record contains the fallback warning, True otherwise
113
+ """
114
+ return "Falling back to parsing as a 'Command'" not in record.getMessage()
115
+
116
+
117
+ def get_logger(name: "str | None" = None) -> logging.Logger:
90
118
  """Get a logger instance with standardized configuration.
91
119
 
92
120
  Args:
@@ -121,3 +149,15 @@ def log_with_context(logger: logging.Logger, level: int, message: str, **extra_f
121
149
  record = logger.makeRecord(logger.name, level, "(unknown file)", 0, message, (), None)
122
150
  record.extra_fields = extra_fields
123
151
  logger.handle(record)
152
+
153
+
154
+ def suppress_erroneous_sqlglot_log_messages() -> None:
155
+ """Suppress confusing sqlglot warning messages.
156
+
157
+ Adds a filter to the sqlglot logger to suppress the warning message
158
+ about falling back to parsing as a Command. This is expected behavior
159
+ in SQLSpec and the warning is confusing to users.
160
+ """
161
+ sqlglot_logger = logging.getLogger("sqlglot")
162
+ if not any(isinstance(f, SqlglotCommandFallbackFilter) for f in sqlglot_logger.filters):
163
+ sqlglot_logger.addFilter(SqlglotCommandFallbackFilter())
@@ -1,15 +1,55 @@
1
1
  """Module loading utilities for SQLSpec.
2
2
 
3
- Provides functions for dynamic module imports and path resolution.
4
- Used for loading modules from dotted paths and converting module paths to filesystem paths.
3
+ Provides functions for dynamic module imports, path resolution, and dependency
4
+ availability checking. Used for loading modules from dotted paths, converting
5
+ module paths to filesystem paths, and ensuring optional dependencies are installed.
5
6
  """
6
7
 
7
8
  import importlib
8
9
  from importlib.util import find_spec
9
10
  from pathlib import Path
10
- from typing import Any, Optional
11
+ from typing import Any
11
12
 
12
- __all__ = ("import_string", "module_to_os_path")
13
+ from sqlspec.exceptions import MissingDependencyError
14
+ from sqlspec.typing import (
15
+ AIOSQL_INSTALLED,
16
+ ATTRS_INSTALLED,
17
+ CATTRS_INSTALLED,
18
+ FSSPEC_INSTALLED,
19
+ LITESTAR_INSTALLED,
20
+ MSGSPEC_INSTALLED,
21
+ NUMPY_INSTALLED,
22
+ OBSTORE_INSTALLED,
23
+ OPENTELEMETRY_INSTALLED,
24
+ ORJSON_INSTALLED,
25
+ PANDAS_INSTALLED,
26
+ PGVECTOR_INSTALLED,
27
+ POLARS_INSTALLED,
28
+ PROMETHEUS_INSTALLED,
29
+ PYARROW_INSTALLED,
30
+ PYDANTIC_INSTALLED,
31
+ )
32
+
33
+ __all__ = (
34
+ "ensure_aiosql",
35
+ "ensure_attrs",
36
+ "ensure_cattrs",
37
+ "ensure_fsspec",
38
+ "ensure_litestar",
39
+ "ensure_msgspec",
40
+ "ensure_numpy",
41
+ "ensure_obstore",
42
+ "ensure_opentelemetry",
43
+ "ensure_orjson",
44
+ "ensure_pandas",
45
+ "ensure_pgvector",
46
+ "ensure_polars",
47
+ "ensure_prometheus",
48
+ "ensure_pyarrow",
49
+ "ensure_pydantic",
50
+ "import_string",
51
+ "module_to_os_path",
52
+ )
13
53
 
14
54
 
15
55
  def module_to_os_path(dotted_path: str = "app") -> "Path":
@@ -46,7 +86,7 @@ def import_string(dotted_path: str) -> "Any":
46
86
  The imported object.
47
87
  """
48
88
 
49
- def _raise_import_error(msg: str, exc: "Optional[Exception]" = None) -> None:
89
+ def _raise_import_error(msg: str, exc: "Exception | None" = None) -> None:
50
90
  if exc is not None:
51
91
  raise ImportError(msg) from exc
52
92
  raise ImportError(msg)
@@ -91,3 +131,163 @@ def import_string(dotted_path: str) -> "Any":
91
131
  except Exception as e: # pylint: disable=broad-exception-caught
92
132
  _raise_import_error(f"Could not import '{dotted_path}': {e}", e)
93
133
  return obj
134
+
135
+
136
+ def ensure_aiosql() -> None:
137
+ """Ensure aiosql is available.
138
+
139
+ Raises:
140
+ MissingDependencyError: If aiosql is not installed.
141
+ """
142
+ if not AIOSQL_INSTALLED:
143
+ raise MissingDependencyError(package="aiosql", install_package="aiosql")
144
+
145
+
146
+ def ensure_attrs() -> None:
147
+ """Ensure attrs is available.
148
+
149
+ Raises:
150
+ MissingDependencyError: If attrs is not installed.
151
+ """
152
+ if not ATTRS_INSTALLED:
153
+ raise MissingDependencyError(package="attrs", install_package="attrs")
154
+
155
+
156
+ def ensure_cattrs() -> None:
157
+ """Ensure cattrs is available.
158
+
159
+ Raises:
160
+ MissingDependencyError: If cattrs is not installed.
161
+ """
162
+ if not CATTRS_INSTALLED:
163
+ raise MissingDependencyError(package="cattrs", install_package="cattrs")
164
+
165
+
166
+ def ensure_fsspec() -> None:
167
+ """Ensure fsspec is available for filesystem operations.
168
+
169
+ Raises:
170
+ MissingDependencyError: If fsspec is not installed.
171
+ """
172
+ if not FSSPEC_INSTALLED:
173
+ raise MissingDependencyError(package="fsspec", install_package="fsspec")
174
+
175
+
176
+ def ensure_litestar() -> None:
177
+ """Ensure Litestar is available.
178
+
179
+ Raises:
180
+ MissingDependencyError: If litestar is not installed.
181
+ """
182
+ if not LITESTAR_INSTALLED:
183
+ raise MissingDependencyError(package="litestar", install_package="litestar")
184
+
185
+
186
+ def ensure_msgspec() -> None:
187
+ """Ensure msgspec is available for serialization.
188
+
189
+ Raises:
190
+ MissingDependencyError: If msgspec is not installed.
191
+ """
192
+ if not MSGSPEC_INSTALLED:
193
+ raise MissingDependencyError(package="msgspec", install_package="msgspec")
194
+
195
+
196
+ def ensure_numpy() -> None:
197
+ """Ensure NumPy is available for array operations.
198
+
199
+ Raises:
200
+ MissingDependencyError: If numpy is not installed.
201
+ """
202
+ if not NUMPY_INSTALLED:
203
+ raise MissingDependencyError(package="numpy", install_package="numpy")
204
+
205
+
206
+ def ensure_obstore() -> None:
207
+ """Ensure obstore is available for object storage operations.
208
+
209
+ Raises:
210
+ MissingDependencyError: If obstore is not installed.
211
+ """
212
+ if not OBSTORE_INSTALLED:
213
+ raise MissingDependencyError(package="obstore", install_package="obstore")
214
+
215
+
216
+ def ensure_opentelemetry() -> None:
217
+ """Ensure OpenTelemetry is available for tracing.
218
+
219
+ Raises:
220
+ MissingDependencyError: If opentelemetry-api is not installed.
221
+ """
222
+ if not OPENTELEMETRY_INSTALLED:
223
+ raise MissingDependencyError(package="opentelemetry-api", install_package="opentelemetry")
224
+
225
+
226
+ def ensure_orjson() -> None:
227
+ """Ensure orjson is available for fast JSON operations.
228
+
229
+ Raises:
230
+ MissingDependencyError: If orjson is not installed.
231
+ """
232
+ if not ORJSON_INSTALLED:
233
+ raise MissingDependencyError(package="orjson", install_package="orjson")
234
+
235
+
236
+ def ensure_pandas() -> None:
237
+ """Ensure pandas is available for DataFrame operations.
238
+
239
+ Raises:
240
+ MissingDependencyError: If pandas is not installed.
241
+ """
242
+ if not PANDAS_INSTALLED:
243
+ raise MissingDependencyError(package="pandas", install_package="pandas")
244
+
245
+
246
+ def ensure_pgvector() -> None:
247
+ """Ensure pgvector is available for vector operations.
248
+
249
+ Raises:
250
+ MissingDependencyError: If pgvector is not installed.
251
+ """
252
+ if not PGVECTOR_INSTALLED:
253
+ raise MissingDependencyError(package="pgvector", install_package="pgvector")
254
+
255
+
256
+ def ensure_polars() -> None:
257
+ """Ensure Polars is available for DataFrame operations.
258
+
259
+ Raises:
260
+ MissingDependencyError: If polars is not installed.
261
+ """
262
+ if not POLARS_INSTALLED:
263
+ raise MissingDependencyError(package="polars", install_package="polars")
264
+
265
+
266
+ def ensure_prometheus() -> None:
267
+ """Ensure Prometheus client is available for metrics.
268
+
269
+ Raises:
270
+ MissingDependencyError: If prometheus-client is not installed.
271
+ """
272
+ if not PROMETHEUS_INSTALLED:
273
+ raise MissingDependencyError(package="prometheus-client", install_package="prometheus")
274
+
275
+
276
+ def ensure_pyarrow() -> None:
277
+ """Ensure PyArrow is available for Arrow operations.
278
+
279
+ Raises:
280
+ MissingDependencyError: If pyarrow is not installed.
281
+ """
282
+ if not PYARROW_INSTALLED:
283
+ raise MissingDependencyError(package="pyarrow", install_package="pyarrow")
284
+
285
+
286
+ def ensure_pydantic() -> None:
287
+ """Ensure Pydantic is available for data validation.
288
+
289
+ Raises:
290
+ MissingDependencyError: If pydantic is not installed.
291
+ """
292
+ if not PYDANTIC_INSTALLED:
293
+ raise MissingDependencyError(package="pydantic", install_package="pydantic")
@@ -0,0 +1,311 @@
1
+ """Portal provider for calling async functions from synchronous contexts.
2
+
3
+ Provides a background thread with an event loop to execute async database operations
4
+ from sync frameworks like Flask. Based on the portal pattern from Advanced Alchemy.
5
+ """
6
+
7
+ import asyncio
8
+ import functools
9
+ import queue
10
+ import threading
11
+ from typing import TYPE_CHECKING, Any, TypeVar
12
+
13
+ from sqlspec.exceptions import ImproperConfigurationError
14
+ from sqlspec.utils.logging import get_logger
15
+ from sqlspec.utils.singleton import SingletonMeta
16
+
17
+ if TYPE_CHECKING:
18
+ from collections.abc import Callable, Coroutine
19
+
20
+ __all__ = ("Portal", "PortalManager", "PortalProvider", "get_global_portal")
21
+
22
+ logger = get_logger("utils.portal")
23
+
24
+ _R = TypeVar("_R")
25
+
26
+
27
+ class PortalProvider:
28
+ """Manages a background thread with event loop for async operations.
29
+
30
+ Creates a daemon thread running an event loop to execute async functions
31
+ from synchronous contexts (Flask routes, etc.).
32
+ """
33
+
34
+ def __init__(self) -> None:
35
+ """Initialize the PortalProvider."""
36
+ self._request_queue: queue.Queue[
37
+ tuple[
38
+ Callable[..., Coroutine[Any, Any, Any]],
39
+ tuple[Any, ...],
40
+ dict[str, Any],
41
+ queue.Queue[tuple[Any | None, Exception | None]],
42
+ ]
43
+ ] = queue.Queue()
44
+ self._loop: asyncio.AbstractEventLoop | None = None
45
+ self._thread: threading.Thread | None = None
46
+ self._ready_event: threading.Event = threading.Event()
47
+
48
+ @property
49
+ def portal(self) -> "Portal":
50
+ """The portal instance for calling async functions.
51
+
52
+ Returns:
53
+ Portal instance.
54
+ """
55
+ return Portal(self)
56
+
57
+ @property
58
+ def is_running(self) -> bool:
59
+ """Check if portal provider is running.
60
+
61
+ Returns:
62
+ True if thread is alive, False otherwise.
63
+ """
64
+ return self._thread is not None and self._thread.is_alive()
65
+
66
+ @property
67
+ def is_ready(self) -> bool:
68
+ """Check if portal provider is ready.
69
+
70
+ Returns:
71
+ True if ready event is set, False otherwise.
72
+ """
73
+ return self._ready_event.is_set()
74
+
75
+ @property
76
+ def loop(self) -> "asyncio.AbstractEventLoop":
77
+ """Get the event loop.
78
+
79
+ Returns:
80
+ The event loop.
81
+
82
+ Raises:
83
+ ImproperConfigurationError: If portal provider not started.
84
+ """
85
+ if self._loop is None:
86
+ msg = "Portal provider not started. Call start() first."
87
+ raise ImproperConfigurationError(msg)
88
+ return self._loop
89
+
90
+ def start(self) -> None:
91
+ """Start the background thread and event loop.
92
+
93
+ Creates a daemon thread running an event loop for async operations.
94
+ """
95
+ if self._thread is not None:
96
+ logger.debug("Portal provider already started")
97
+ return
98
+
99
+ self._thread = threading.Thread(target=self._run_event_loop, daemon=True)
100
+ self._thread.start()
101
+ self._ready_event.wait()
102
+ logger.debug("Portal provider started")
103
+
104
+ def stop(self) -> None:
105
+ """Stop the background thread and event loop.
106
+
107
+ Gracefully shuts down the event loop and waits for thread to finish.
108
+ """
109
+ if self._loop is None or self._thread is None:
110
+ logger.debug("Portal provider not running")
111
+ return
112
+
113
+ self._loop.call_soon_threadsafe(self._loop.stop)
114
+ self._thread.join(timeout=5)
115
+
116
+ if self._thread.is_alive():
117
+ logger.warning("Portal thread did not stop within 5 seconds")
118
+
119
+ self._loop.close()
120
+ self._loop = None
121
+ self._thread = None
122
+ self._ready_event.clear()
123
+ logger.debug("Portal provider stopped")
124
+
125
+ def _run_event_loop(self) -> None:
126
+ """Main function of the background thread.
127
+
128
+ Creates event loop and runs forever until stopped.
129
+ """
130
+ if self._loop is None:
131
+ self._loop = asyncio.new_event_loop()
132
+
133
+ asyncio.set_event_loop(self._loop)
134
+ self._ready_event.set()
135
+ self._loop.run_forever()
136
+
137
+ @staticmethod
138
+ async def _async_caller(
139
+ func: "Callable[..., Coroutine[Any, Any, _R]]", args: "tuple[Any, ...]", kwargs: "dict[str, Any]"
140
+ ) -> _R:
141
+ """Wrapper to run async function.
142
+
143
+ Args:
144
+ func: The async function to call.
145
+ args: Positional arguments.
146
+ kwargs: Keyword arguments.
147
+
148
+ Returns:
149
+ Result of the async function.
150
+ """
151
+ result: _R = await func(*args, **kwargs)
152
+ return result
153
+
154
+ def call(self, func: "Callable[..., Coroutine[Any, Any, _R]]", *args: Any, **kwargs: Any) -> _R:
155
+ """Call an async function from synchronous context.
156
+
157
+ Executes the async function in the background event loop and blocks
158
+ until the result is available.
159
+
160
+ Args:
161
+ func: The async function to call.
162
+ *args: Positional arguments to the function.
163
+ **kwargs: Keyword arguments to the function.
164
+
165
+ Returns:
166
+ Result of the async function.
167
+
168
+ Raises:
169
+ ImproperConfigurationError: If portal provider not started.
170
+ """
171
+ if self._loop is None:
172
+ msg = "Portal provider not started. Call start() first."
173
+ raise ImproperConfigurationError(msg)
174
+
175
+ local_result_queue: queue.Queue[tuple[_R | None, Exception | None]] = queue.Queue()
176
+
177
+ self._request_queue.put((func, args, kwargs, local_result_queue))
178
+
179
+ self._loop.call_soon_threadsafe(self._process_request)
180
+
181
+ result, exception = local_result_queue.get()
182
+
183
+ if exception:
184
+ raise exception
185
+ return result # type: ignore[return-value]
186
+
187
+ def _process_request(self) -> None:
188
+ """Process a request from the request queue in the event loop."""
189
+ if self._loop is None:
190
+ return
191
+
192
+ if not self._request_queue.empty():
193
+ func, args, kwargs, local_result_queue = self._request_queue.get()
194
+ future = asyncio.run_coroutine_threadsafe(self._async_caller(func, args, kwargs), self._loop)
195
+
196
+ future.add_done_callback(
197
+ functools.partial(self._handle_future_result, local_result_queue=local_result_queue) # pyright: ignore[reportArgumentType]
198
+ )
199
+
200
+ @staticmethod
201
+ def _handle_future_result(
202
+ future: "asyncio.Future[Any]", local_result_queue: "queue.Queue[tuple[Any | None, Exception | None]]"
203
+ ) -> None:
204
+ """Handle result or exception from completed future.
205
+
206
+ Args:
207
+ future: The completed future.
208
+ local_result_queue: Queue to put result in.
209
+ """
210
+ try:
211
+ result = future.result()
212
+ local_result_queue.put((result, None))
213
+ except Exception as exc:
214
+ local_result_queue.put((None, exc))
215
+
216
+
217
+ class Portal:
218
+ """Portal for calling async functions using PortalProvider."""
219
+
220
+ def __init__(self, provider: "PortalProvider") -> None:
221
+ """Initialize Portal with provider.
222
+
223
+ Args:
224
+ provider: The portal provider instance.
225
+ """
226
+ self._provider = provider
227
+
228
+ def call(self, func: "Callable[..., Coroutine[Any, Any, _R]]", *args: Any, **kwargs: Any) -> _R:
229
+ """Call an async function using the portal provider.
230
+
231
+ Args:
232
+ func: The async function to call.
233
+ *args: Positional arguments to the function.
234
+ **kwargs: Keyword arguments to the function.
235
+
236
+ Returns:
237
+ Result of the async function.
238
+ """
239
+ return self._provider.call(func, *args, **kwargs)
240
+
241
+
242
+ class PortalManager(metaclass=SingletonMeta):
243
+ """Singleton manager for global portal instance.
244
+
245
+ Provides a global portal for use by sync_tools and other utilities
246
+ that need to call async functions from synchronous contexts without
247
+ an existing event loop.
248
+
249
+ Example:
250
+ manager = PortalManager()
251
+ portal = manager.get_or_create_portal()
252
+ result = portal.call(some_async_function, arg1, arg2)
253
+ """
254
+
255
+ def __init__(self) -> None:
256
+ """Initialize the PortalManager singleton."""
257
+ self._provider: PortalProvider | None = None
258
+ self._portal: Portal | None = None
259
+ self._lock = threading.Lock()
260
+
261
+ def get_or_create_portal(self) -> Portal:
262
+ """Get or create the global portal instance.
263
+
264
+ Lazily creates and starts the portal provider on first access.
265
+ Thread-safe via locking.
266
+
267
+ Returns:
268
+ Global portal instance.
269
+ """
270
+ if self._portal is None:
271
+ with self._lock:
272
+ if self._portal is None:
273
+ self._provider = PortalProvider()
274
+ self._provider.start()
275
+ self._portal = Portal(self._provider)
276
+ logger.debug("Global portal provider created and started")
277
+
278
+ return self._portal
279
+
280
+ @property
281
+ def is_running(self) -> bool:
282
+ """Check if global portal is running.
283
+
284
+ Returns:
285
+ True if portal provider exists and is running, False otherwise.
286
+ """
287
+ return self._provider is not None and self._provider.is_running
288
+
289
+ def stop(self) -> None:
290
+ """Stop the global portal provider.
291
+
292
+ Should typically only be called during application shutdown.
293
+ """
294
+ if self._provider is not None:
295
+ self._provider.stop()
296
+ self._provider = None
297
+ self._portal = None
298
+ logger.debug("Global portal provider stopped")
299
+
300
+
301
+ def get_global_portal() -> Portal:
302
+ """Get the global portal instance for async-to-sync bridging.
303
+
304
+ Convenience function that creates and returns the singleton portal.
305
+ Used by sync_tools and other utilities.
306
+
307
+ Returns:
308
+ Global portal instance.
309
+ """
310
+ manager = PortalManager()
311
+ return manager.get_or_create_portal()