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
@@ -0,0 +1,543 @@
1
+ """Application dependency providers for FastAPI filter injection.
2
+
3
+ This module provides filter dependency injection for FastAPI routes, allowing
4
+ automatic parsing of query parameters into SQLSpec filter objects.
5
+ """
6
+
7
+ import datetime
8
+ import inspect
9
+ from typing import TYPE_CHECKING, Annotated, Any, Literal, NamedTuple, cast
10
+ from uuid import UUID
11
+
12
+ from fastapi import Depends, Query
13
+ from fastapi.exceptions import RequestValidationError
14
+ from typing_extensions import NotRequired, TypedDict
15
+
16
+ from sqlspec.core.filters import (
17
+ BeforeAfterFilter,
18
+ FilterTypes,
19
+ InCollectionFilter,
20
+ LimitOffsetFilter,
21
+ NotInCollectionFilter,
22
+ OrderByFilter,
23
+ SearchFilter,
24
+ )
25
+ from sqlspec.utils.singleton import SingletonMeta
26
+ from sqlspec.utils.text import camelize
27
+
28
+ if TYPE_CHECKING:
29
+ from collections.abc import Callable
30
+
31
+ __all__ = (
32
+ "DEPENDENCY_DEFAULTS",
33
+ "BooleanOrNone",
34
+ "DTorNone",
35
+ "DependencyDefaults",
36
+ "FieldNameType",
37
+ "FilterConfig",
38
+ "HashableType",
39
+ "HashableValue",
40
+ "IntOrNone",
41
+ "SortOrder",
42
+ "SortOrderOrNone",
43
+ "StringOrNone",
44
+ "UuidOrNone",
45
+ "dep_cache",
46
+ "provide_filters",
47
+ )
48
+
49
+ DTorNone = datetime.datetime | None
50
+ StringOrNone = str | None
51
+ UuidOrNone = UUID | None
52
+ IntOrNone = int | None
53
+ BooleanOrNone = bool | None
54
+ SortOrder = Literal["asc", "desc"]
55
+ SortOrderOrNone = SortOrder | None
56
+ HashableValue = str | int | float | bool | None
57
+ HashableType = HashableValue | tuple[Any, ...] | tuple[tuple[str, Any], ...] | tuple[HashableValue, ...]
58
+
59
+
60
+ class DependencyDefaults:
61
+ """Default values for dependency generation."""
62
+
63
+ CREATED_FILTER_DEPENDENCY_KEY: str = "created_filter"
64
+ ID_FILTER_DEPENDENCY_KEY: str = "id_filter"
65
+ LIMIT_OFFSET_FILTER_DEPENDENCY_KEY: str = "limit_offset_filter"
66
+ UPDATED_FILTER_DEPENDENCY_KEY: str = "updated_filter"
67
+ ORDER_BY_FILTER_DEPENDENCY_KEY: str = "order_by_filter"
68
+ SEARCH_FILTER_DEPENDENCY_KEY: str = "search_filter"
69
+ DEFAULT_PAGINATION_SIZE: int = 20
70
+
71
+
72
+ DEPENDENCY_DEFAULTS = DependencyDefaults()
73
+
74
+
75
+ class FieldNameType(NamedTuple):
76
+ """Type for field name and associated type information for filter configuration."""
77
+
78
+ name: str
79
+ """Name of the field to filter on."""
80
+ type_hint: type[Any] = str
81
+ """Type of the filter value. Defaults to str."""
82
+
83
+
84
+ class FilterConfig(TypedDict):
85
+ """Configuration for generating dynamic filters for FastAPI."""
86
+
87
+ id_filter: NotRequired[type[UUID | int | str]]
88
+ """Type of ID filter to enable (UUID, int, or str). When set, enables collection filtering by IDs."""
89
+ id_field: NotRequired[str]
90
+ """Field name for ID filtering. Defaults to 'id'."""
91
+ sort_field: NotRequired[str | set[str]]
92
+ """Default field(s) to use for sorting."""
93
+ sort_order: NotRequired[SortOrder]
94
+ """Default sort order ('asc' or 'desc'). Defaults to 'desc'."""
95
+ pagination_type: NotRequired[Literal["limit_offset"]]
96
+ """When set to 'limit_offset', enables pagination with page size and current page parameters."""
97
+ pagination_size: NotRequired[int]
98
+ """Default pagination page size. Defaults to DEFAULT_PAGINATION_SIZE (20)."""
99
+ search: NotRequired[str | set[str]]
100
+ """Field(s) to enable search filtering on. Can be comma-separated string or set of field names."""
101
+ search_ignore_case: NotRequired[bool]
102
+ """When True, search is case-insensitive. Defaults to False."""
103
+ created_at: NotRequired[bool]
104
+ """When True, enables created_at date range filtering. Uses 'created_at' field by default."""
105
+ updated_at: NotRequired[bool]
106
+ """When True, enables updated_at date range filtering. Uses 'updated_at' field by default."""
107
+ not_in_fields: NotRequired[FieldNameType | set[FieldNameType]]
108
+ """Fields that support not-in collection filtering. Can be single field or set of fields with type info."""
109
+ in_fields: NotRequired[FieldNameType | set[FieldNameType]]
110
+ """Fields that support in-collection filtering. Can be single field or set of fields with type info."""
111
+
112
+
113
+ class DependencyCache(metaclass=SingletonMeta):
114
+ """Simple dependency cache to memoize dynamically generated dependencies."""
115
+
116
+ def __init__(self) -> None:
117
+ self.dependencies: dict[int, Callable[..., list[FilterTypes]]] = {}
118
+
119
+ def add_dependencies(self, key: int, dependencies: "Callable[..., list[FilterTypes]]") -> None:
120
+ """Add dependencies to cache.
121
+
122
+ Args:
123
+ key: Cache key (hash of config).
124
+ dependencies: Dependency callable to cache.
125
+ """
126
+ self.dependencies[key] = dependencies
127
+
128
+ def get_dependencies(self, key: int) -> "Callable[..., list[FilterTypes]] | None":
129
+ """Get dependencies from cache.
130
+
131
+ Args:
132
+ key: Cache key (hash of config).
133
+
134
+ Returns:
135
+ Cached dependency callable or None if not found.
136
+ """
137
+ return self.dependencies.get(key)
138
+
139
+
140
+ dep_cache = DependencyCache()
141
+
142
+
143
+ def provide_filters(
144
+ config: FilterConfig, dep_defaults: DependencyDefaults = DEPENDENCY_DEFAULTS
145
+ ) -> "Callable[..., list[FilterTypes]]":
146
+ """Create FastAPI dependency provider for filters based on configuration.
147
+
148
+ This function dynamically generates a FastAPI dependency function that parses
149
+ query parameters into SQLSpec filter objects.
150
+
151
+ Args:
152
+ config: Filter configuration specifying which filters to enable.
153
+ dep_defaults: Dependency defaults for filter configuration.
154
+
155
+ Returns:
156
+ A FastAPI dependency callable that returns list of filters.
157
+
158
+ Example:
159
+ from fastapi import Depends, FastAPI
160
+ from sqlspec.extensions.fastapi import SQLSpecPlugin, FilterConfig
161
+
162
+ app = FastAPI()
163
+ db_ext = SQLSpecPlugin(sql, app)
164
+
165
+ @app.get("/users")
166
+ async def list_users(
167
+ filters = Depends(
168
+ db_ext.provide_filters({
169
+ "id_filter": UUID,
170
+ "search": "name,email",
171
+ "pagination_type": "limit_offset",
172
+ })
173
+ ),
174
+ ):
175
+ stmt = sql("SELECT * FROM users")
176
+ for filter in filters:
177
+ stmt = filter.append_to_statement(stmt)
178
+ result = await db.execute(stmt)
179
+ return result.all()
180
+ """
181
+ filter_keys = {
182
+ "id_filter",
183
+ "created_at",
184
+ "updated_at",
185
+ "pagination_type",
186
+ "search",
187
+ "sort_field",
188
+ "not_in_fields",
189
+ "in_fields",
190
+ }
191
+
192
+ has_filters = False
193
+ for key in filter_keys:
194
+ value = config.get(key)
195
+ if value is not None and value is not False and value != []:
196
+ has_filters = True
197
+ break
198
+
199
+ if not has_filters:
200
+ return lambda: cast("list[FilterTypes]", [])
201
+
202
+ cache_key = hash(_make_hashable(config))
203
+
204
+ cached_dep = dep_cache.get_dependencies(cache_key)
205
+ if cached_dep is not None:
206
+ return cached_dep
207
+
208
+ dep = _create_filter_aggregate_function_fastapi(config, dep_defaults)
209
+ dep_cache.add_dependencies(cache_key, dep)
210
+ return dep
211
+
212
+
213
+ def _make_hashable(value: Any) -> HashableType:
214
+ """Convert a value into a hashable type for caching purposes.
215
+
216
+ Args:
217
+ value: Any value that needs to be made hashable.
218
+
219
+ Returns:
220
+ A hashable version of the value.
221
+ """
222
+ if isinstance(value, dict):
223
+ items = []
224
+ for k in sorted(value.keys()):
225
+ v = value[k]
226
+ items.append((str(k), _make_hashable(v)))
227
+ return tuple(items)
228
+ if isinstance(value, (list, set)):
229
+ hashable_items = [_make_hashable(item) for item in value]
230
+ filtered_items = [item for item in hashable_items if item is not None]
231
+ return tuple(sorted(filtered_items, key=str))
232
+ if isinstance(value, (str, int, float, bool, type(None))):
233
+ return value
234
+ return str(value)
235
+
236
+
237
+ def _create_filter_aggregate_function_fastapi( # noqa: C901
238
+ config: FilterConfig, dep_defaults: DependencyDefaults = DEPENDENCY_DEFAULTS
239
+ ) -> "Callable[..., list[FilterTypes]]":
240
+ """Create a FastAPI dependency function that aggregates multiple filter dependencies.
241
+
242
+ Args:
243
+ config: Filter configuration.
244
+ dep_defaults: Dependency defaults.
245
+
246
+ Returns:
247
+ A FastAPI dependency function that aggregates multiple filter dependencies.
248
+ """
249
+ params: list[inspect.Parameter] = []
250
+ annotations: dict[str, Any] = {}
251
+
252
+ if config.get("id_filter", False) is not False:
253
+
254
+ def provide_id_filter(
255
+ ids: Annotated[list[Any] | None, Query(alias="ids", description="IDs to filter by.")] = None,
256
+ ) -> InCollectionFilter[Any] | None:
257
+ return InCollectionFilter(field_name=config.get("id_field", "id"), values=ids) if ids else None
258
+
259
+ params.append(
260
+ inspect.Parameter(
261
+ name=dep_defaults.ID_FILTER_DEPENDENCY_KEY,
262
+ kind=inspect.Parameter.KEYWORD_ONLY,
263
+ annotation=Annotated["InCollectionFilter[Any] | None", Depends(provide_id_filter)],
264
+ )
265
+ )
266
+ annotations[dep_defaults.ID_FILTER_DEPENDENCY_KEY] = Annotated[
267
+ "InCollectionFilter[Any] | None", Depends(provide_id_filter)
268
+ ]
269
+
270
+ if config.get("created_at", False):
271
+
272
+ def provide_created_at_filter(
273
+ before: Annotated[
274
+ str | None,
275
+ Query(
276
+ alias="createdBefore",
277
+ description="Filter by created date before this timestamp.",
278
+ json_schema_extra={"format": "date-time"},
279
+ ),
280
+ ] = None,
281
+ after: Annotated[
282
+ str | None,
283
+ Query(
284
+ alias="createdAfter",
285
+ description="Filter by created date after this timestamp.",
286
+ json_schema_extra={"format": "date-time"},
287
+ ),
288
+ ] = None,
289
+ ) -> "BeforeAfterFilter | None":
290
+ before_dt = None
291
+ after_dt = None
292
+
293
+ if before is not None:
294
+ try:
295
+ before_dt = datetime.datetime.fromisoformat(before.replace("Z", "+00:00"))
296
+ except (ValueError, TypeError, AttributeError):
297
+ msg = "Invalid date format for createdBefore"
298
+ raise RequestValidationError(
299
+ errors=[{"loc": ("query", "createdBefore"), "msg": msg, "type": "value_error.datetime"}]
300
+ )
301
+
302
+ if after is not None:
303
+ try:
304
+ after_dt = datetime.datetime.fromisoformat(after.replace("Z", "+00:00"))
305
+ except (ValueError, TypeError, AttributeError):
306
+ msg = "Invalid date format for createdAfter"
307
+ raise RequestValidationError(
308
+ errors=[{"loc": ("query", "createdAfter"), "msg": msg, "type": "value_error.datetime"}]
309
+ )
310
+
311
+ return (
312
+ BeforeAfterFilter(field_name="created_at", before=before_dt, after=after_dt)
313
+ if before_dt or after_dt
314
+ else None
315
+ )
316
+
317
+ param_name = dep_defaults.CREATED_FILTER_DEPENDENCY_KEY
318
+ params.append(
319
+ inspect.Parameter(
320
+ name=param_name,
321
+ kind=inspect.Parameter.KEYWORD_ONLY,
322
+ annotation=Annotated["BeforeAfterFilter | None", Depends(provide_created_at_filter)],
323
+ )
324
+ )
325
+ annotations[param_name] = Annotated["BeforeAfterFilter | None", Depends(provide_created_at_filter)]
326
+
327
+ if config.get("updated_at", False):
328
+
329
+ def provide_updated_at_filter(
330
+ before: Annotated[
331
+ str | None,
332
+ Query(
333
+ alias="updatedBefore",
334
+ description="Filter by updated date before this timestamp.",
335
+ json_schema_extra={"format": "date-time"},
336
+ ),
337
+ ] = None,
338
+ after: Annotated[
339
+ str | None,
340
+ Query(
341
+ alias="updatedAfter",
342
+ description="Filter by updated date after this timestamp.",
343
+ json_schema_extra={"format": "date-time"},
344
+ ),
345
+ ] = None,
346
+ ) -> "BeforeAfterFilter | None":
347
+ before_dt = None
348
+ after_dt = None
349
+
350
+ if before is not None:
351
+ try:
352
+ before_dt = datetime.datetime.fromisoformat(before.replace("Z", "+00:00"))
353
+ except (ValueError, TypeError, AttributeError):
354
+ msg = "Invalid date format for updatedBefore"
355
+ raise RequestValidationError(
356
+ errors=[{"loc": ("query", "updatedBefore"), "msg": msg, "type": "value_error.datetime"}]
357
+ )
358
+
359
+ if after is not None:
360
+ try:
361
+ after_dt = datetime.datetime.fromisoformat(after.replace("Z", "+00:00"))
362
+ except (ValueError, TypeError, AttributeError):
363
+ msg = "Invalid date format for updatedAfter"
364
+ raise RequestValidationError(
365
+ errors=[{"loc": ("query", "updatedAfter"), "msg": msg, "type": "value_error.datetime"}]
366
+ )
367
+
368
+ return (
369
+ BeforeAfterFilter(field_name="updated_at", before=before_dt, after=after_dt)
370
+ if before_dt or after_dt
371
+ else None
372
+ )
373
+
374
+ param_name = dep_defaults.UPDATED_FILTER_DEPENDENCY_KEY
375
+ params.append(
376
+ inspect.Parameter(
377
+ name=param_name,
378
+ kind=inspect.Parameter.KEYWORD_ONLY,
379
+ annotation=Annotated["BeforeAfterFilter | None", Depends(provide_updated_at_filter)],
380
+ )
381
+ )
382
+ annotations[param_name] = Annotated["BeforeAfterFilter | None", Depends(provide_updated_at_filter)]
383
+
384
+ if config.get("pagination_type") == "limit_offset":
385
+
386
+ def provide_limit_offset_pagination(
387
+ current_page: Annotated[
388
+ int, Query(ge=1, alias="currentPage", description="Page number for pagination.")
389
+ ] = 1,
390
+ page_size: Annotated[
391
+ int, Query(ge=1, alias="pageSize", description="Number of items per page.")
392
+ ] = config.get("pagination_size", dep_defaults.DEFAULT_PAGINATION_SIZE),
393
+ ) -> LimitOffsetFilter:
394
+ return LimitOffsetFilter(limit=page_size, offset=page_size * (current_page - 1))
395
+
396
+ param_name = dep_defaults.LIMIT_OFFSET_FILTER_DEPENDENCY_KEY
397
+ params.append(
398
+ inspect.Parameter(
399
+ name=param_name,
400
+ kind=inspect.Parameter.KEYWORD_ONLY,
401
+ annotation=Annotated[LimitOffsetFilter, Depends(provide_limit_offset_pagination)],
402
+ )
403
+ )
404
+ annotations[param_name] = Annotated[LimitOffsetFilter, Depends(provide_limit_offset_pagination)]
405
+
406
+ if search_fields := config.get("search"):
407
+
408
+ def provide_search_filter(
409
+ search_string: Annotated[str | None, Query(alias="searchString", description="Search term.")] = None,
410
+ ignore_case: Annotated[
411
+ bool | None, Query(alias="searchIgnoreCase", description="Whether search should be case-insensitive.")
412
+ ] = config.get("search_ignore_case", False),
413
+ ) -> "SearchFilter | None":
414
+ field_names = set(search_fields.split(",")) if isinstance(search_fields, str) else search_fields
415
+
416
+ return (
417
+ SearchFilter(field_name=field_names, value=search_string, ignore_case=ignore_case or False)
418
+ if search_string
419
+ else None
420
+ )
421
+
422
+ param_name = dep_defaults.SEARCH_FILTER_DEPENDENCY_KEY
423
+ params.append(
424
+ inspect.Parameter(
425
+ name=param_name,
426
+ kind=inspect.Parameter.KEYWORD_ONLY,
427
+ annotation=Annotated["SearchFilter | None", Depends(provide_search_filter)],
428
+ )
429
+ )
430
+ annotations[param_name] = Annotated["SearchFilter | None", Depends(provide_search_filter)]
431
+
432
+ if sort_field := config.get("sort_field"):
433
+ sort_order_default = config.get("sort_order", "desc")
434
+ default_field = sort_field if isinstance(sort_field, str) else next(iter(sort_field))
435
+
436
+ def provide_order_by(
437
+ field_name: Annotated[str, Query(alias="orderBy", description="Field to order by.")] = default_field,
438
+ sort_order: Annotated[
439
+ SortOrder | None, Query(alias="sortOrder", description="Sort order ('asc' or 'desc').")
440
+ ] = sort_order_default,
441
+ ) -> OrderByFilter:
442
+ return OrderByFilter(field_name=field_name, sort_order=sort_order or sort_order_default)
443
+
444
+ param_name = dep_defaults.ORDER_BY_FILTER_DEPENDENCY_KEY
445
+ params.append(
446
+ inspect.Parameter(
447
+ name=param_name,
448
+ kind=inspect.Parameter.KEYWORD_ONLY,
449
+ annotation=Annotated[OrderByFilter, Depends(provide_order_by)],
450
+ )
451
+ )
452
+ annotations[param_name] = Annotated[OrderByFilter, Depends(provide_order_by)]
453
+
454
+ if not_in_fields := config.get("not_in_fields"):
455
+ not_in_fields = {not_in_fields} if isinstance(not_in_fields, (str, FieldNameType)) else not_in_fields
456
+ for field_def in not_in_fields:
457
+
458
+ def create_not_in_filter_provider(
459
+ field_name: FieldNameType = field_def,
460
+ ) -> "Callable[..., NotInCollectionFilter[Any] | None]":
461
+ def provide_not_in_filter(
462
+ values: Annotated[
463
+ set[Any] | None,
464
+ Query(
465
+ alias=camelize(f"{field_name.name}_not_in"),
466
+ description=f"Filter {field_name.name} not in values",
467
+ ),
468
+ ] = None,
469
+ ) -> "NotInCollectionFilter[Any] | None":
470
+ return NotInCollectionFilter(field_name=field_name.name, values=values) if values else None
471
+
472
+ return provide_not_in_filter
473
+
474
+ not_in_provider = create_not_in_filter_provider()
475
+ param_name = f"{field_def.name}_not_in_filter"
476
+ params.append(
477
+ inspect.Parameter(
478
+ name=param_name,
479
+ kind=inspect.Parameter.KEYWORD_ONLY,
480
+ annotation=Annotated["NotInCollectionFilter[Any] | None", Depends(not_in_provider)],
481
+ )
482
+ )
483
+ annotations[param_name] = Annotated["NotInCollectionFilter[Any] | None", Depends(not_in_provider)]
484
+
485
+ if in_fields := config.get("in_fields"):
486
+ in_fields = {in_fields} if isinstance(in_fields, (str, FieldNameType)) else in_fields
487
+ for field_def in in_fields:
488
+
489
+ def create_in_filter_provider(
490
+ field_name: FieldNameType = field_def,
491
+ ) -> "Callable[..., InCollectionFilter[Any] | None]":
492
+ def provide_in_filter(
493
+ values: Annotated[
494
+ set[Any] | None,
495
+ Query(
496
+ alias=camelize(f"{field_name.name}_in"), description=f"Filter {field_name.name} in values"
497
+ ),
498
+ ] = None,
499
+ ) -> "InCollectionFilter[Any] | None":
500
+ return InCollectionFilter(field_name=field_name.name, values=values) if values else None
501
+
502
+ return provide_in_filter
503
+
504
+ in_provider = create_in_filter_provider()
505
+ param_name = f"{field_def.name}_in_filter"
506
+ params.append(
507
+ inspect.Parameter(
508
+ name=param_name,
509
+ kind=inspect.Parameter.KEYWORD_ONLY,
510
+ annotation=Annotated["InCollectionFilter[Any] | None", Depends(in_provider)],
511
+ )
512
+ )
513
+ annotations[param_name] = Annotated["InCollectionFilter[Any] | None", Depends(in_provider)]
514
+
515
+ _aggregate_filter_function.__signature__ = inspect.Signature( # type: ignore[attr-defined]
516
+ parameters=params, return_annotation=Annotated["list[FilterTypes]", _aggregate_filter_function]
517
+ )
518
+
519
+ return _aggregate_filter_function
520
+
521
+
522
+ def _aggregate_filter_function(**kwargs: Any) -> "list[FilterTypes]":
523
+ """Aggregate filter dependencies based on configuration.
524
+
525
+ Args:
526
+ **kwargs: Filter parameters dynamically provided based on configuration.
527
+
528
+ Returns:
529
+ List of configured filters.
530
+ """
531
+ filters: list[FilterTypes] = []
532
+ for filter_value in kwargs.values():
533
+ if filter_value is None:
534
+ continue
535
+ if isinstance(filter_value, list):
536
+ filters.extend(filter_value)
537
+ elif (isinstance(filter_value, SearchFilter) and filter_value.value is None) or (
538
+ isinstance(filter_value, OrderByFilter) and filter_value.field_name is None
539
+ ):
540
+ continue
541
+ else:
542
+ filters.append(filter_value)
543
+ return filters
@@ -0,0 +1,36 @@
1
+ """Flask extension for SQLSpec.
2
+
3
+ Provides request-scoped session management, automatic transaction handling,
4
+ and async adapter support via portal pattern.
5
+
6
+ Example:
7
+ from flask import Flask
8
+ from sqlspec import SQLSpec
9
+ from sqlspec.adapters.sqlite import SqliteConfig
10
+ from sqlspec.extensions.flask import SQLSpecPlugin
11
+
12
+ sqlspec = SQLSpec()
13
+ config = SqliteConfig(
14
+ pool_config={"database": "app.db"},
15
+ extension_config={
16
+ "flask": {
17
+ "commit_mode": "autocommit",
18
+ "session_key": "db"
19
+ }
20
+ }
21
+ )
22
+ sqlspec.add_config(config)
23
+
24
+ app = Flask(__name__)
25
+ plugin = SQLSpecPlugin(sqlspec, app)
26
+
27
+ @app.route("/users")
28
+ def list_users():
29
+ db = plugin.get_session()
30
+ result = db.execute("SELECT * FROM users")
31
+ return {"users": result.all()}
32
+ """
33
+
34
+ from sqlspec.extensions.flask.extension import SQLSpecPlugin
35
+
36
+ __all__ = ("SQLSpecPlugin",)
@@ -0,0 +1,71 @@
1
+ """Flask configuration state management."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, Any, Literal
5
+
6
+ if TYPE_CHECKING:
7
+ from sqlspec.config import DatabaseConfigProtocol
8
+
9
+ __all__ = ("FlaskConfigState",)
10
+
11
+ HTTP_SUCCESS_MIN = 200
12
+ HTTP_SUCCESS_MAX = 300
13
+ HTTP_REDIRECT_MAX = 400
14
+
15
+
16
+ @dataclass
17
+ class FlaskConfigState:
18
+ """Internal state for each database configuration in Flask extension.
19
+
20
+ Holds configuration, state keys, commit settings, and transaction logic.
21
+ """
22
+
23
+ config: "DatabaseConfigProtocol[Any, Any, Any]"
24
+ connection_key: str
25
+ session_key: str
26
+ commit_mode: Literal["manual", "autocommit", "autocommit_include_redirect"]
27
+ extra_commit_statuses: "set[int] | None"
28
+ extra_rollback_statuses: "set[int] | None"
29
+ is_async: bool
30
+
31
+ def should_commit(self, status_code: int) -> bool:
32
+ """Determine if HTTP status code should trigger commit.
33
+
34
+ Args:
35
+ status_code: HTTP response status code.
36
+
37
+ Returns:
38
+ True if status should trigger commit, False otherwise.
39
+ """
40
+ if self.extra_commit_statuses and status_code in self.extra_commit_statuses:
41
+ return True
42
+
43
+ if self.extra_rollback_statuses and status_code in self.extra_rollback_statuses:
44
+ return False
45
+
46
+ if self.commit_mode == "manual":
47
+ return False
48
+
49
+ if self.commit_mode == "autocommit":
50
+ return HTTP_SUCCESS_MIN <= status_code < HTTP_SUCCESS_MAX
51
+
52
+ if self.commit_mode == "autocommit_include_redirect":
53
+ return HTTP_SUCCESS_MIN <= status_code < HTTP_REDIRECT_MAX
54
+
55
+ return False
56
+
57
+ def should_rollback(self, status_code: int) -> bool:
58
+ """Determine if HTTP status code should trigger rollback.
59
+
60
+ In autocommit modes, anything that doesn't commit should rollback.
61
+
62
+ Args:
63
+ status_code: HTTP response status code.
64
+
65
+ Returns:
66
+ True if status should trigger rollback, False otherwise.
67
+ """
68
+ if self.commit_mode == "manual":
69
+ return False
70
+
71
+ return not self.should_commit(status_code)
@@ -0,0 +1,40 @@
1
+ """Helper utilities for Flask extension."""
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ if TYPE_CHECKING:
6
+ from sqlspec.extensions.flask._state import FlaskConfigState
7
+ from sqlspec.utils.portal import Portal
8
+
9
+ __all__ = ("get_or_create_session",)
10
+
11
+
12
+ def get_or_create_session(config_state: "FlaskConfigState", portal: "Portal | None") -> Any:
13
+ """Get or create database session for current request.
14
+
15
+ Sessions are cached per request in Flask g object to ensure
16
+ the same session is reused throughout the request lifecycle.
17
+
18
+ Args:
19
+ config_state: Configuration state for this database.
20
+ portal: Portal for async operations (None for sync).
21
+
22
+ Returns:
23
+ Database session (driver instance).
24
+ """
25
+ from flask import g
26
+
27
+ cache_key = f"sqlspec_session_cache_{config_state.session_key}"
28
+
29
+ cached_session = getattr(g, cache_key, None)
30
+ if cached_session is not None:
31
+ return cached_session
32
+
33
+ connection = getattr(g, config_state.connection_key)
34
+
35
+ session = config_state.config.driver_type(
36
+ connection=connection, statement_config=config_state.config.statement_config
37
+ )
38
+
39
+ setattr(g, cache_key, session)
40
+ return session