sqlspec 0.3.0__tar.gz → 0.4.0__tar.gz

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 (60) hide show
  1. {sqlspec-0.3.0 → sqlspec-0.4.0}/CONTRIBUTING.rst +4 -4
  2. {sqlspec-0.3.0 → sqlspec-0.4.0}/PKG-INFO +1 -1
  3. {sqlspec-0.3.0 → sqlspec-0.4.0}/pyproject.toml +20 -8
  4. {sqlspec-0.3.0 → sqlspec-0.4.0}/sqlspec/_typing.py +27 -1
  5. {sqlspec-0.3.0 → sqlspec-0.4.0}/sqlspec/adapters/adbc/config.py +1 -3
  6. {sqlspec-0.3.0 → sqlspec-0.4.0}/sqlspec/adapters/aiosqlite/config.py +2 -19
  7. {sqlspec-0.3.0 → sqlspec-0.4.0}/sqlspec/adapters/asyncmy/config.py +3 -21
  8. {sqlspec-0.3.0 → sqlspec-0.4.0}/sqlspec/adapters/asyncpg/config.py +3 -13
  9. sqlspec-0.4.0/sqlspec/adapters/duckdb/config.py +201 -0
  10. {sqlspec-0.3.0 → sqlspec-0.4.0}/sqlspec/adapters/oracledb/config/_asyncio.py +8 -11
  11. {sqlspec-0.3.0 → sqlspec-0.4.0}/sqlspec/adapters/oracledb/config/_common.py +2 -2
  12. {sqlspec-0.3.0 → sqlspec-0.4.0}/sqlspec/adapters/oracledb/config/_sync.py +2 -9
  13. {sqlspec-0.3.0 → sqlspec-0.4.0}/sqlspec/adapters/psycopg/config/_async.py +2 -11
  14. {sqlspec-0.3.0 → sqlspec-0.4.0}/sqlspec/adapters/psycopg/config/_common.py +3 -2
  15. {sqlspec-0.3.0 → sqlspec-0.4.0}/sqlspec/adapters/psycopg/config/_sync.py +2 -11
  16. {sqlspec-0.3.0 → sqlspec-0.4.0}/sqlspec/adapters/sqlite/config.py +3 -20
  17. {sqlspec-0.3.0 → sqlspec-0.4.0}/sqlspec/filters.py +6 -4
  18. {sqlspec-0.3.0 → sqlspec-0.4.0}/sqlspec/typing.py +131 -3
  19. sqlspec-0.4.0/tests/unit/test_adapters/test_duckdb/__init__.py +0 -0
  20. sqlspec-0.4.0/tests/unit/test_adapters/test_duckdb/test_config.py +252 -0
  21. sqlspec-0.4.0/tests/unit/test_typing.py +276 -0
  22. sqlspec-0.4.0/tools/__init__.py +0 -0
  23. {sqlspec-0.3.0 → sqlspec-0.4.0}/uv.lock +4 -4
  24. sqlspec-0.3.0/sqlspec/adapters/duckdb/config.py +0 -101
  25. sqlspec-0.3.0/sqlspec/utils/dataclass.py +0 -138
  26. sqlspec-0.3.0/sqlspec/utils/empty.py +0 -18
  27. {sqlspec-0.3.0 → sqlspec-0.4.0}/.gitignore +0 -0
  28. {sqlspec-0.3.0 → sqlspec-0.4.0}/.pre-commit-config.yaml +0 -0
  29. {sqlspec-0.3.0 → sqlspec-0.4.0}/Makefile +0 -0
  30. {sqlspec-0.3.0 → sqlspec-0.4.0}/NOTICE +0 -0
  31. {sqlspec-0.3.0 → sqlspec-0.4.0}/README.md +0 -0
  32. {sqlspec-0.3.0 → sqlspec-0.4.0}/sqlspec/__init__.py +0 -0
  33. {sqlspec-0.3.0 → sqlspec-0.4.0}/sqlspec/__metadata__.py +0 -0
  34. {sqlspec-0.3.0 → sqlspec-0.4.0}/sqlspec/_serialization.py +0 -0
  35. {sqlspec-0.3.0 → sqlspec-0.4.0}/sqlspec/adapters/__init__.py +0 -0
  36. {sqlspec-0.3.0 → sqlspec-0.4.0}/sqlspec/adapters/adbc/__init__.py +0 -0
  37. {sqlspec-0.3.0 → sqlspec-0.4.0}/sqlspec/adapters/aiosqlite/__init__.py +0 -0
  38. {sqlspec-0.3.0 → sqlspec-0.4.0}/sqlspec/adapters/asyncmy/__init__.py +0 -0
  39. {sqlspec-0.3.0 → sqlspec-0.4.0}/sqlspec/adapters/asyncpg/__init__.py +0 -0
  40. {sqlspec-0.3.0 → sqlspec-0.4.0}/sqlspec/adapters/duckdb/__init__.py +0 -0
  41. {sqlspec-0.3.0 → sqlspec-0.4.0}/sqlspec/adapters/oracledb/__init__.py +0 -0
  42. {sqlspec-0.3.0 → sqlspec-0.4.0}/sqlspec/adapters/oracledb/config/__init__.py +0 -0
  43. {sqlspec-0.3.0 → sqlspec-0.4.0}/sqlspec/adapters/psycopg/__init__.py +0 -0
  44. {sqlspec-0.3.0 → sqlspec-0.4.0}/sqlspec/adapters/psycopg/config/__init__.py +0 -0
  45. {sqlspec-0.3.0 → sqlspec-0.4.0}/sqlspec/adapters/sqlite/__init__.py +0 -0
  46. {sqlspec-0.3.0 → sqlspec-0.4.0}/sqlspec/config.py +0 -0
  47. {sqlspec-0.3.0 → sqlspec-0.4.0}/sqlspec/exceptions.py +0 -0
  48. {sqlspec-0.3.0 → sqlspec-0.4.0}/sqlspec/extensions/__init__.py +0 -0
  49. {sqlspec-0.3.0 → sqlspec-0.4.0}/sqlspec/extensions/litestar/__init__.py +0 -0
  50. {sqlspec-0.3.0 → sqlspec-0.4.0}/sqlspec/extensions/litestar/plugin.py +0 -0
  51. {sqlspec-0.3.0 → sqlspec-0.4.0}/sqlspec/py.typed +0 -0
  52. {sqlspec-0.3.0/sqlspec/utils → sqlspec-0.4.0/tests}/__init__.py +0 -0
  53. {sqlspec-0.3.0 → sqlspec-0.4.0}/tests/conftest.py +0 -0
  54. {sqlspec-0.3.0/tests → sqlspec-0.4.0/tests/unit}/__init__.py +0 -0
  55. {sqlspec-0.3.0/tools → sqlspec-0.4.0/tests/unit/test_adapters}/__init__.py +0 -0
  56. {sqlspec-0.3.0 → sqlspec-0.4.0}/tools/build_docs.py +0 -0
  57. {sqlspec-0.3.0 → sqlspec-0.4.0}/tools/pypi_readme.py +0 -0
  58. {sqlspec-0.3.0 → sqlspec-0.4.0}/tools/sphinx_ext/__init__.py +0 -0
  59. {sqlspec-0.3.0 → sqlspec-0.4.0}/tools/sphinx_ext/changelog.py +0 -0
  60. {sqlspec-0.3.0 → sqlspec-0.4.0}/tools/sphinx_ext/missing_references.py +0 -0
@@ -14,7 +14,7 @@ Code contributions
14
14
  Workflow
15
15
  ++++++++
16
16
 
17
- 1. `Fork <https://github.com/litestar-org/litestar-htmx/fork>`_ the `litestar-htmx repository <https://github.com/litestar-org/litestar-htmx>`_
17
+ 1. `Fork <https://github.com/litestar-org/sqlspec/fork>`_ the `sqlspec repository <https://github.com/litestar-org/sqlspec>`_
18
18
  2. Clone your fork locally with git
19
19
  3. `Set up the environment <#setting-up-the-environment>`_
20
20
  4. Make your changes
@@ -59,13 +59,13 @@ You can serve the documentation with ``make docs-serve``, or build them with ``m
59
59
  Creating a new release
60
60
  ----------------------
61
61
 
62
- 1. Increment the version in `pyproject.toml <https://github.com/litestar-org/litestar-htmx/blob/main/pyproject.toml>`_.
62
+ 1. Increment the version in `pyproject.toml <https://github.com/litestar-org/sqlspec/blob/main/pyproject.toml>`_.
63
63
  .. note:: The version should follow `semantic versioning <https://semver.org/>`_ and `PEP 440 <https://www.python.org/dev/peps/pep-0440/>`_.
64
- 2. `Draft a new release <https://github.com/litestar-org/litestar-htmx/releases/new>`_ on GitHub
64
+ 2. `Draft a new release <https://github.com/litestar-org/sqlspec/releases/new>`_ on GitHub
65
65
 
66
66
  * Use ``vMAJOR.MINOR.PATCH`` (e.g. ``v1.2.3``) as both the tag and release title
67
67
  * Fill in the release description. You can use the "Generate release notes" function to get a draft for this
68
68
  3. Commit your changes and push to ``main``
69
69
  4. Publish the release
70
- 5. Go to `Actions <https://github.com/litestar-org/litestar-htmx/actions>`_ and approve the release workflow
70
+ 5. Go to `Actions <https://github.com/litestar-org/sqlspec/actions>`_ and approve the release workflow
71
71
  6. Check that the workflow runs successfully
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlspec
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: SQL Experiments in Python
5
5
  Author-email: Cody Fincher <cody@litestar.dev>
6
6
  Maintainer-email: Litestar Developers <hello@litestar.dev>
@@ -6,7 +6,7 @@ maintainers = [{ name = "Litestar Developers", email = "hello@litestar.dev" }]
6
6
  name = "sqlspec"
7
7
  readme = "README.md"
8
8
  requires-python = ">=3.9, <4.0"
9
- version = "0.3.0"
9
+ version = "0.4.0"
10
10
 
11
11
  [project.optional-dependencies]
12
12
  adbc = ["adbc-driver-manager", "pyarrow"]
@@ -128,6 +128,8 @@ exclude_lines = [
128
128
 
129
129
  [tool.pytest.ini_options]
130
130
  addopts = "-ra -q --doctest-glob='*.md' --strict-markers --strict-config"
131
+ asyncio_default_fixture_loop_scope = "function"
132
+ asyncio_mode = "auto"
131
133
  testpaths = ["tests"]
132
134
  xfail_strict = true
133
135
 
@@ -149,7 +151,17 @@ warn_unused_ignores = true
149
151
 
150
152
  [[tool.mypy.overrides]]
151
153
  ignore_missing_imports = true
152
- module = ["orjson", "re2", "re2.*", "uvicorn.*", "googleapiclient", "googleapiclient.*", "uvloop.*","asyncmy", "asyncmy.*"]
154
+ module = [
155
+ "orjson",
156
+ "re2",
157
+ "re2.*",
158
+ "uvicorn.*",
159
+ "googleapiclient",
160
+ "googleapiclient.*",
161
+ "uvloop.*",
162
+ "asyncmy",
163
+ "asyncmy.*",
164
+ ]
153
165
 
154
166
  [tool.pyright]
155
167
  disableBytesTypePromotions = true
@@ -272,25 +284,25 @@ docstring-code-line-length = 88
272
284
  [tool.git-cliff.changelog]
273
285
  body = """
274
286
  {% if version %}\
275
- `Release [v{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} <https://github.com/litestar-org/litestar-htmx/releases/tag/v{{ version | trim_start_matches(pat="v") }}>`_
287
+ `Release [v{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} <https://github.com/litestar-org/sqlspec/releases/tag/v{{ version | trim_start_matches(pat="v") }}>`_
276
288
  ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
277
- * `See All commits in v{{ version | trim_start_matches(pat="v") }} <https://github.com/litestar-org/litestar-htmx/commits/v{{ version | trim_start_matches(pat="v") }}>`_
289
+ * `See All commits in v{{ version | trim_start_matches(pat="v") }} <https://github.com/litestar-org/sqlspec/commits/v{{ version | trim_start_matches(pat="v") }}>`_
278
290
  {% else %}\
279
291
  [unreleased]
280
292
  ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
281
293
  {% endif %}\
282
294
  {% if previous %}\
283
295
  {% if previous.commit_id %}
284
- `{{ previous.commit_id | truncate(length=7, end="") }} <https://github.com/litestar-org/litestar-htmx/commit/{{ previous.commit_id }}>`_ ... \
285
- `{{ commit_id | truncate(length=7, end="") }} <https://github.com/litestar-org/litestar-htmx/commit/{{ commit_id }}>`_ \
286
- | `See diff for {{ version | trim_start_matches(pat="v") }} <https://github.com/litestar-org/litestar-htmx/compare/{{ previous.commit_id }}...{{ commit_id }}>`_
296
+ `{{ previous.commit_id | truncate(length=7, end="") }} <https://github.com/litestar-org/sqlspec/commit/{{ previous.commit_id }}>`_ ... \
297
+ `{{ commit_id | truncate(length=7, end="") }} <https://github.com/litestar-org/sqlspec/commit/{{ commit_id }}>`_ \
298
+ | `See diff for {{ version | trim_start_matches(pat="v") }} <https://github.com/litestar-org/sqlspec/compare/{{ previous.commit_id }}...{{ commit_id }}>`_
287
299
  {% endif %}\
288
300
  {% endif %}\
289
301
  {% for group, commits in commits | group_by(attribute="group") %}
290
302
  {{ group | upper_first }}
291
303
  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
292
304
  {% for commit in commits %}
293
- * (`{{ commit.id | truncate(length=7, end="") }} <https://github.com/litestar-org/litestar-htmx/commit/{{ commit.id }}>`_) {% if commit.breaking %}[**breaking**] {% endif %} - {{ commit.message | upper_first }} ({{ commit.author.name }})\
305
+ * (`{{ commit.id | truncate(length=7, end="") }} <https://github.com/litestar-org/sqlspec/commit/{{ commit.id }}>`_) {% if commit.breaking %}[**breaking**] {% endif %} - {{ commit.message | upper_first }} ({{ commit.author.name }})\
294
306
  {% for footer in commit.footers -%}
295
307
  , {{ footer.token }}{{ footer.separator }}{{ footer.value }}\
296
308
  {% endfor %}\
@@ -5,15 +5,26 @@ This is used to ensure compatibility when one or more of the libraries are insta
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
+ from enum import Enum
8
9
  from typing import (
9
10
  Any,
10
11
  ClassVar,
12
+ Final,
11
13
  Protocol,
14
+ Union,
12
15
  cast,
13
16
  runtime_checkable,
14
17
  )
15
18
 
16
- from typing_extensions import TypeVar, dataclass_transform
19
+ from typing_extensions import Literal, TypeVar, dataclass_transform
20
+
21
+
22
+ @runtime_checkable
23
+ class DataclassProtocol(Protocol):
24
+ """Protocol for instance checking dataclasses."""
25
+
26
+ __dataclass_fields__: ClassVar[dict[str, Any]]
27
+
17
28
 
18
29
  T = TypeVar("T")
19
30
  T_co = TypeVar("T_co", covariant=True)
@@ -99,11 +110,26 @@ except ImportError:
99
110
  UNSET = UnsetType.UNSET # pyright: ignore[reportConstantRedefinition]
100
111
  MSGSPEC_INSTALLED = False # pyright: ignore[reportConstantRedefinition]
101
112
 
113
+
114
+ class EmptyEnum(Enum):
115
+ """A sentinel enum used as placeholder."""
116
+
117
+ EMPTY = 0
118
+
119
+
120
+ EmptyType = Union[Literal[EmptyEnum.EMPTY], UnsetType]
121
+ Empty: Final = EmptyEnum.EMPTY
122
+
123
+
102
124
  __all__ = (
103
125
  "MSGSPEC_INSTALLED",
104
126
  "PYDANTIC_INSTALLED",
105
127
  "UNSET",
106
128
  "BaseModel",
129
+ "DataclassProtocol",
130
+ "Empty",
131
+ "EmptyEnum",
132
+ "EmptyType",
107
133
  "FailFast",
108
134
  "Struct",
109
135
  "TypeAdapter",
@@ -5,7 +5,7 @@ from dataclasses import dataclass
5
5
  from typing import TYPE_CHECKING, TypeVar
6
6
 
7
7
  from sqlspec.config import GenericDatabaseConfig
8
- from sqlspec.utils.empty import Empty
8
+ from sqlspec.typing import Empty, EmptyType
9
9
 
10
10
  if TYPE_CHECKING:
11
11
  from collections.abc import Generator
@@ -13,8 +13,6 @@ if TYPE_CHECKING:
13
13
 
14
14
  from adbc_driver_manager.dbapi import Connection, Cursor
15
15
 
16
- from sqlspec.utils.empty import EmptyType
17
-
18
16
  __all__ = ("AdbcDatabaseConfig",)
19
17
 
20
18
  ConnectionT = TypeVar("ConnectionT", bound="Connection")
@@ -6,8 +6,7 @@ from typing import TYPE_CHECKING, Any
6
6
 
7
7
  from sqlspec.config import GenericDatabaseConfig
8
8
  from sqlspec.exceptions import ImproperConfigurationError
9
- from sqlspec.utils.dataclass import simple_asdict
10
- from sqlspec.utils.empty import Empty, EmptyType
9
+ from sqlspec.typing import Empty, EmptyType, dataclass_to_dict
11
10
 
12
11
  if TYPE_CHECKING:
13
12
  from collections.abc import AsyncGenerator
@@ -60,7 +59,7 @@ class AiosqliteConfig(GenericDatabaseConfig):
60
59
  Returns:
61
60
  A string keyed dict of config kwargs for the aiosqlite.connect() function.
62
61
  """
63
- return simple_asdict(self, exclude_empty=True, convert_nested=False)
62
+ return dataclass_to_dict(self, exclude_empty=True, convert_nested=False)
64
63
 
65
64
  async def create_connection(self) -> Connection:
66
65
  """Create and return a new database connection.
@@ -79,22 +78,6 @@ class AiosqliteConfig(GenericDatabaseConfig):
79
78
  msg = f"Could not configure the Aiosqlite connection. Error: {e!s}"
80
79
  raise ImproperConfigurationError(msg) from e
81
80
 
82
- @asynccontextmanager
83
- async def lifespan(self, *args: Any, **kwargs: Any) -> AsyncGenerator[None, None]:
84
- """Manage the lifecycle of a database connection.
85
-
86
- Yields:
87
- None
88
-
89
- Raises:
90
- ImproperConfigurationError: If the connection could not be established.
91
- """
92
- connection = await self.create_connection()
93
- try:
94
- yield
95
- finally:
96
- await connection.close()
97
-
98
81
  @asynccontextmanager
99
82
  async def provide_connection(self, *args: Any, **kwargs: Any) -> AsyncGenerator[Connection, None]:
100
83
  """Create and provide a database connection.
@@ -5,8 +5,7 @@ from dataclasses import dataclass
5
5
  from typing import TYPE_CHECKING, TypeVar
6
6
 
7
7
  from sqlspec.exceptions import ImproperConfigurationError
8
- from sqlspec.utils.dataclass import simple_asdict
9
- from sqlspec.utils.empty import Empty, EmptyType
8
+ from sqlspec.typing import Empty, EmptyType, dataclass_to_dict
10
9
 
11
10
  if TYPE_CHECKING:
12
11
  from collections.abc import AsyncGenerator
@@ -101,7 +100,7 @@ class AsyncmyPoolConfig:
101
100
  Returns:
102
101
  A string keyed dict of config kwargs for the Asyncmy create_pool function.
103
102
  """
104
- return simple_asdict(self, exclude_empty=True, convert_nested=False)
103
+ return dataclass_to_dict(self, exclude_empty=True, convert_nested=False)
105
104
 
106
105
 
107
106
  @dataclass
@@ -125,7 +124,7 @@ class AsyncMyConfig:
125
124
  A string keyed dict of config kwargs for the Asyncmy create_pool function.
126
125
  """
127
126
  if self.pool_config:
128
- return simple_asdict(self.pool_config, exclude_empty=True, convert_nested=False)
127
+ return dataclass_to_dict(self.pool_config, exclude_empty=True, convert_nested=False)
129
128
  msg = "'pool_config' methods can not be used when a 'pool_instance' is provided."
130
129
  raise ImproperConfigurationError(msg)
131
130
 
@@ -162,23 +161,6 @@ class AsyncMyConfig:
162
161
  """
163
162
  return await self.create_pool()
164
163
 
165
- @asynccontextmanager
166
- async def lifespan(self, *args: Any, **kwargs: Any) -> AsyncGenerator[None, None]:
167
- """Manage the lifecycle of a database connection pool.
168
-
169
- Yields:
170
- None
171
-
172
- Raises:
173
- ImproperConfigurationError: If the pool could not be established.
174
- """
175
- pool = await self.create_pool()
176
- try:
177
- yield
178
- finally:
179
- pool.close()
180
- await pool.wait_closed()
181
-
182
164
  @asynccontextmanager
183
165
  async def provide_connection(self, *args: Any, **kwargs: Any) -> AsyncGenerator[Connection, None]:
184
166
  """Create and provide a database connection.
@@ -10,8 +10,7 @@ from asyncpg import create_pool as asyncpg_create_pool
10
10
  from sqlspec._serialization import decode_json, encode_json
11
11
  from sqlspec.config import GenericDatabaseConfig, GenericPoolConfig
12
12
  from sqlspec.exceptions import ImproperConfigurationError
13
- from sqlspec.utils.dataclass import simple_asdict
14
- from sqlspec.utils.empty import Empty, EmptyType
13
+ from sqlspec.typing import Empty, EmptyType, dataclass_to_dict
15
14
 
16
15
  if TYPE_CHECKING:
17
16
  from asyncio import AbstractEventLoop
@@ -98,7 +97,7 @@ class AsyncPgConfig(GenericDatabaseConfig):
98
97
  function.
99
98
  """
100
99
  if self.pool_config:
101
- return simple_asdict(self.pool_config, exclude_empty=True, convert_nested=False)
100
+ return dataclass_to_dict(self.pool_config, exclude_empty=True, convert_nested=False)
102
101
  msg = "'pool_config' methods can not be used when a 'pool_instance' is provided."
103
102
  raise ImproperConfigurationError(msg)
104
103
 
@@ -124,16 +123,7 @@ class AsyncPgConfig(GenericDatabaseConfig):
124
123
  )
125
124
  return self.pool_instance
126
125
 
127
- @asynccontextmanager
128
- async def lifespan(self, *args: Any, **kwargs) -> AsyncGenerator[None, None]:
129
- db_pool = await self.create_pool()
130
- try:
131
- yield
132
- finally:
133
- db_pool.terminate()
134
- await db_pool.close()
135
-
136
- def provide_pool(self, *args: Any, **kwargs) -> Awaitable[Pool]:
126
+ def provide_pool(self, *args: Any, **kwargs: Any) -> Awaitable[Pool]:
137
127
  """Create a pool instance.
138
128
 
139
129
  Returns:
@@ -0,0 +1,201 @@
1
+ from __future__ import annotations
2
+
3
+ from contextlib import contextmanager
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING, Any, cast
6
+
7
+ from sqlspec.config import GenericDatabaseConfig
8
+ from sqlspec.exceptions import ImproperConfigurationError
9
+ from sqlspec.typing import Empty, EmptyType, dataclass_to_dict
10
+
11
+ if TYPE_CHECKING:
12
+ from collections.abc import Generator, Sequence
13
+
14
+ from duckdb import DuckDBPyConnection
15
+
16
+ __all__ = ("DuckDBConfig", "ExtensionConfig")
17
+
18
+
19
+ @dataclass
20
+ class ExtensionConfig:
21
+ """Configuration for a DuckDB extension.
22
+
23
+ This class provides configuration options for DuckDB extensions, including installation
24
+ and post-install configuration settings.
25
+
26
+ Args:
27
+ name: The name of the extension to install
28
+ config: Optional configuration settings to apply after installation
29
+ force_install: Whether to force reinstall if already present
30
+ repository: Optional repository name to install from
31
+ repository_url: Optional repository URL to install from
32
+ version: Optional version of the extension to install
33
+ """
34
+
35
+ name: str
36
+ config: dict[str, Any] | None = None
37
+ force_install: bool = False
38
+ repository: str | None = None
39
+ repository_url: str | None = None
40
+ version: str | None = None
41
+
42
+ @classmethod
43
+ def from_dict(cls, name: str, config: dict[str, Any] | bool | None = None) -> ExtensionConfig:
44
+ """Create an ExtensionConfig from a configuration dictionary.
45
+
46
+ Args:
47
+ name: The name of the extension
48
+ config: Configuration dictionary that may contain settings
49
+
50
+ Returns:
51
+ A new ExtensionConfig instance
52
+ """
53
+ if config is None:
54
+ return cls(name=name)
55
+
56
+ if not isinstance(config, dict):
57
+ config = {"force_install": bool(config)}
58
+
59
+ install_args = {
60
+ key: config.pop(key)
61
+ for key in ["force_install", "repository", "repository_url", "version", "config", "name"]
62
+ if key in config
63
+ }
64
+ return cls(name=name, **install_args)
65
+
66
+
67
+ @dataclass
68
+ class DuckDBConfig(GenericDatabaseConfig):
69
+ """Configuration for DuckDB database connections.
70
+
71
+ This class provides configuration options for DuckDB database connections, wrapping all parameters
72
+ available to duckdb.connect().
73
+
74
+ For details see: https://duckdb.org/docs/api/python/overview#connection-options
75
+ """
76
+
77
+ database: str | EmptyType = Empty
78
+ """The path to the database file to be opened. Pass ":memory:" to open a connection to a database that resides in RAM instead of on disk. If not specified, an in-memory database will be created."""
79
+
80
+ read_only: bool | EmptyType = Empty
81
+ """If True, the database will be opened in read-only mode. This is required if multiple processes want to access the same database file at the same time."""
82
+
83
+ config: dict[str, Any] | EmptyType = Empty
84
+ """A dictionary of configuration options to be passed to DuckDB. These can include settings like 'access_mode', 'max_memory', 'threads', etc.
85
+
86
+ For details see: https://duckdb.org/docs/api/python/overview#connection-options
87
+ """
88
+
89
+ extensions: Sequence[ExtensionConfig] | EmptyType = Empty
90
+ """A sequence of extension configurations to install and configure upon connection creation."""
91
+
92
+ def __post_init__(self) -> None:
93
+ """Post-initialization validation and processing.
94
+
95
+ This method handles merging extension configurations from both the extensions field
96
+ and the config dictionary, if present. The config['extensions'] field can be either:
97
+ - A dictionary mapping extension names to their configurations
98
+ - A list of extension names (which will be installed with force_install=True)
99
+
100
+ Raises:
101
+ ImproperConfigurationError: If there are duplicate extension configurations.
102
+ """
103
+ if self.config is Empty:
104
+ self.config = {}
105
+
106
+ if self.extensions is Empty:
107
+ self.extensions = []
108
+ # this is purely for mypy
109
+ assert isinstance(self.config, dict) # noqa: S101
110
+ assert isinstance(self.extensions, list) # noqa: S101
111
+
112
+ _e = self.config.pop("extensions", {})
113
+ if not isinstance(_e, (dict, list, tuple)):
114
+ msg = "When configuring extensions in the 'config' dictionary, the value must be a dictionary or sequence of extension names"
115
+ raise ImproperConfigurationError(msg)
116
+ if not isinstance(_e, dict):
117
+ _e = {str(ext): {"force_install": False} for ext in _e}
118
+
119
+ if len(set(_e.keys()).intersection({ext.name for ext in self.extensions})) > 0:
120
+ msg = "Configuring the same extension in both 'extensions' and as a key in 'config['extensions']' is not allowed"
121
+ raise ImproperConfigurationError(msg)
122
+
123
+ self.extensions.extend([ExtensionConfig.from_dict(name, ext_config) for name, ext_config in _e.items()])
124
+
125
+ def _configure_extensions(self, connection: DuckDBPyConnection) -> None:
126
+ """Configure extensions for the connection.
127
+
128
+ Args:
129
+ connection: The DuckDB connection to configure extensions for.
130
+
131
+ Raises:
132
+ ImproperConfigurationError: If extension installation or configuration fails.
133
+ """
134
+ if self.extensions is Empty:
135
+ return
136
+
137
+ for extension in cast("list[ExtensionConfig]", self.extensions):
138
+ try:
139
+ if extension.force_install:
140
+ connection.install_extension(
141
+ extension=extension.name,
142
+ force_install=extension.force_install,
143
+ repository=extension.repository,
144
+ repository_url=extension.repository_url,
145
+ version=extension.version,
146
+ )
147
+ connection.load_extension(extension.name)
148
+
149
+ if extension.config:
150
+ for key, value in extension.config.items():
151
+ connection.execute(f"SET {key}={value}")
152
+ except Exception as e:
153
+ msg = f"Failed to configure extension {extension.name}. Error: {e!s}"
154
+ raise ImproperConfigurationError(msg) from e
155
+
156
+ @property
157
+ def connection_config_dict(self) -> dict[str, Any]:
158
+ """Return the connection configuration as a dict.
159
+
160
+ Returns:
161
+ A string keyed dict of config kwargs for the duckdb.connect() function.
162
+ """
163
+ config = dataclass_to_dict(self, exclude_empty=True, exclude={"extensions"}, convert_nested=False)
164
+ if not config.get("database"):
165
+ config["database"] = ":memory:"
166
+ return config
167
+
168
+ def create_connection(self) -> DuckDBPyConnection:
169
+ """Create and return a new database connection with configured extensions.
170
+
171
+ Returns:
172
+ A new DuckDB connection instance with extensions installed and configured.
173
+
174
+ Raises:
175
+ ImproperConfigurationError: If the connection could not be established or extensions could not be configured.
176
+ """
177
+ import duckdb
178
+
179
+ try:
180
+ connection = duckdb.connect(**self.connection_config_dict)
181
+ self._configure_extensions(connection)
182
+ return connection
183
+ except Exception as e:
184
+ msg = f"Could not configure the DuckDB connection. Error: {e!s}"
185
+ raise ImproperConfigurationError(msg) from e
186
+
187
+ @contextmanager
188
+ def provide_connection(self, *args: Any, **kwargs: Any) -> Generator[DuckDBPyConnection, None, None]:
189
+ """Create and provide a database connection.
190
+
191
+ Yields:
192
+ A DuckDB connection instance.
193
+
194
+ Raises:
195
+ ImproperConfigurationError: If the connection could not be established.
196
+ """
197
+ connection = self.create_connection()
198
+ try:
199
+ yield connection
200
+ finally:
201
+ connection.close()
@@ -13,7 +13,7 @@ from sqlspec.adapters.oracledb.config._common import (
13
13
  OracleGenericPoolConfig,
14
14
  )
15
15
  from sqlspec.exceptions import ImproperConfigurationError
16
- from sqlspec.utils.dataclass import simple_asdict
16
+ from sqlspec.typing import dataclass_to_dict
17
17
 
18
18
  if TYPE_CHECKING:
19
19
  from collections.abc import AsyncGenerator, Awaitable
@@ -36,6 +36,11 @@ class OracleAsyncDatabaseConfig(OracleGenericDatabaseConfig[AsyncConnectionPool,
36
36
 
37
37
  pool_config: OracleAsyncPoolConfig | None = None
38
38
  """Oracle Pool configuration"""
39
+ pool_instance: AsyncConnectionPool | None = None
40
+ """Optional pool to use.
41
+
42
+ If set, the plugin will use the provided pool rather than instantiate one.
43
+ """
39
44
 
40
45
  @property
41
46
  def pool_config_dict(self) -> dict[str, Any]:
@@ -46,7 +51,7 @@ class OracleAsyncDatabaseConfig(OracleGenericDatabaseConfig[AsyncConnectionPool,
46
51
  function.
47
52
  """
48
53
  if self.pool_config is not None:
49
- return simple_asdict(self.pool_config, exclude_empty=True, convert_nested=False)
54
+ return dataclass_to_dict(self.pool_config, exclude_empty=True, convert_nested=False)
50
55
  msg = "'pool_config' methods can not be used when a 'pool_instance' is provided."
51
56
  raise ImproperConfigurationError(msg)
52
57
 
@@ -70,15 +75,7 @@ class OracleAsyncDatabaseConfig(OracleGenericDatabaseConfig[AsyncConnectionPool,
70
75
  raise ImproperConfigurationError(msg)
71
76
  return self.pool_instance
72
77
 
73
- @asynccontextmanager
74
- async def lifespan(self, *args: Any, **kwargs) -> AsyncGenerator[None, None]:
75
- db_pool = await self.create_pool()
76
- try:
77
- yield
78
- finally:
79
- await db_pool.close(force=True)
80
-
81
- def provide_pool(self, *args: Any, **kwargs) -> Awaitable[AsyncConnectionPool]:
78
+ def provide_pool(self, *args: Any, **kwargs: Any) -> Awaitable[AsyncConnectionPool]:
82
79
  """Create a pool instance.
83
80
 
84
81
  Returns:
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Generic, TypeVar
6
6
  from oracledb import ConnectionPool
7
7
 
8
8
  from sqlspec.config import GenericDatabaseConfig, GenericPoolConfig
9
- from sqlspec.utils.empty import Empty
9
+ from sqlspec.typing import Empty
10
10
 
11
11
  if TYPE_CHECKING:
12
12
  import ssl
@@ -17,7 +17,7 @@ if TYPE_CHECKING:
17
17
  from oracledb.connection import AsyncConnection, Connection
18
18
  from oracledb.pool import AsyncConnectionPool, ConnectionPool
19
19
 
20
- from sqlspec.utils.empty import EmptyType
20
+ from sqlspec.typing import EmptyType
21
21
 
22
22
  __all__ = (
23
23
  "OracleGenericDatabaseConfig",
@@ -13,7 +13,7 @@ from sqlspec.adapters.oracledb.config._common import (
13
13
  OracleGenericPoolConfig,
14
14
  )
15
15
  from sqlspec.exceptions import ImproperConfigurationError
16
- from sqlspec.utils.dataclass import simple_asdict
16
+ from sqlspec.typing import dataclass_to_dict
17
17
 
18
18
  if TYPE_CHECKING:
19
19
  from collections.abc import Generator
@@ -51,7 +51,7 @@ class OracleSyncDatabaseConfig(OracleGenericDatabaseConfig[ConnectionPool, Conne
51
51
  function.
52
52
  """
53
53
  if self.pool_config:
54
- return simple_asdict(self.pool_config, exclude_empty=True, convert_nested=False)
54
+ return dataclass_to_dict(self.pool_config, exclude_empty=True, convert_nested=False)
55
55
  msg = "'pool_config' methods can not be used when a 'pool_instance' is provided."
56
56
  raise ImproperConfigurationError(msg)
57
57
 
@@ -75,13 +75,6 @@ class OracleSyncDatabaseConfig(OracleGenericDatabaseConfig[ConnectionPool, Conne
75
75
  raise ImproperConfigurationError(msg)
76
76
  return self.pool_instance
77
77
 
78
- def lifespan(self, *args: Any, **kwargs: Any) -> Generator[None, None, None]:
79
- db_pool = self.create_pool()
80
- try:
81
- yield
82
- finally:
83
- db_pool.close()
84
-
85
78
  def provide_pool(self, *args: Any, **kwargs: Any) -> ConnectionPool:
86
79
  """Create a pool instance.
87
80
 
@@ -12,7 +12,7 @@ from sqlspec.adapters.psycopg.config._common import (
12
12
  PsycoPgGenericPoolConfig,
13
13
  )
14
14
  from sqlspec.exceptions import ImproperConfigurationError
15
- from sqlspec.utils.dataclass import simple_asdict
15
+ from sqlspec.typing import dataclass_to_dict
16
16
 
17
17
  if TYPE_CHECKING:
18
18
  from collections.abc import AsyncGenerator, Awaitable
@@ -43,7 +43,7 @@ class PsycoPgAsyncDatabaseConfig(PsycoPgGenericDatabaseConfig[AsyncConnectionPoo
43
43
  def pool_config_dict(self) -> dict[str, Any]:
44
44
  """Return the pool configuration as a dict."""
45
45
  if self.pool_config:
46
- return simple_asdict(self.pool_config, exclude_empty=True, convert_nested=False)
46
+ return dataclass_to_dict(self.pool_config, exclude_empty=True, convert_nested=False)
47
47
  msg = "'pool_config' methods can not be used when a 'pool_instance' is provided."
48
48
  raise ImproperConfigurationError(msg)
49
49
 
@@ -63,15 +63,6 @@ class PsycoPgAsyncDatabaseConfig(PsycoPgGenericDatabaseConfig[AsyncConnectionPoo
63
63
  raise ImproperConfigurationError(msg)
64
64
  return self.pool_instance
65
65
 
66
- @asynccontextmanager
67
- async def lifespan(self, *args: Any, **kwargs: Any) -> AsyncGenerator[None, None]:
68
- """Manage the lifecycle of the connection pool."""
69
- pool = await self.create_pool()
70
- try:
71
- yield
72
- finally:
73
- await pool.close()
74
-
75
66
  def provide_pool(self, *args: Any, **kwargs: Any) -> Awaitable[AsyncConnectionPool]:
76
67
  """Create and return a connection pool."""
77
68
  return self.create_pool()
@@ -4,7 +4,7 @@ from dataclasses import dataclass
4
4
  from typing import TYPE_CHECKING, Generic, TypeVar
5
5
 
6
6
  from sqlspec.config import GenericDatabaseConfig, GenericPoolConfig
7
- from sqlspec.utils.empty import Empty
7
+ from sqlspec.typing import Empty
8
8
 
9
9
  if TYPE_CHECKING:
10
10
  from collections.abc import Callable
@@ -13,7 +13,8 @@ if TYPE_CHECKING:
13
13
  from psycopg import AsyncConnection, Connection
14
14
  from psycopg_pool import AsyncConnectionPool, ConnectionPool
15
15
 
16
- from sqlspec.utils.empty import EmptyType
16
+ from sqlspec.typing import EmptyType
17
+
17
18
 
18
19
  __all__ = (
19
20
  "PsycoPgGenericDatabaseConfig",