sqlalchemy-foundation-kit 0.2.0__tar.gz → 0.3.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.
Files changed (54) hide show
  1. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/CHANGELOG.md +14 -0
  2. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/PKG-INFO +18 -5
  3. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/README.md +16 -4
  4. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/pyproject.toml +5 -0
  5. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/__init__.py +1 -1
  6. sqlalchemy_foundation_kit-0.3.0/sqlalchemy_foundation_kit/__version__.py +1 -0
  7. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/base/_optional.py +2 -2
  8. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/base/serialization.py +2 -2
  9. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/config/postgres.py +3 -3
  10. sqlalchemy_foundation_kit-0.3.0/sqlalchemy_foundation_kit/contrib/dependency_injector/_deps.py +59 -0
  11. sqlalchemy_foundation_kit-0.3.0/sqlalchemy_foundation_kit/contrib/di/_deps.py +53 -0
  12. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/settings/postgres.py +13 -4
  13. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/session/builder.py +18 -0
  14. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/session/factories.py +12 -3
  15. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/session/locks.py +29 -1
  16. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/session/manager.py +49 -7
  17. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/uow/sqlalchemy.py +43 -12
  18. sqlalchemy_foundation_kit-0.2.0/sqlalchemy_foundation_kit/__version__.py +0 -1
  19. sqlalchemy_foundation_kit-0.2.0/sqlalchemy_foundation_kit/contrib/dependency_injector/_deps.py +0 -37
  20. sqlalchemy_foundation_kit-0.2.0/sqlalchemy_foundation_kit/contrib/di/_deps.py +0 -32
  21. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/.gitignore +0 -0
  22. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/LICENSE +0 -0
  23. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/_typing.py +0 -0
  24. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/base/__init__.py +0 -0
  25. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/base/engine.py +0 -0
  26. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/base/metadata.py +0 -0
  27. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/base/models.py +0 -0
  28. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/base/types.py +0 -0
  29. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/config/__init__.py +0 -0
  30. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/__init__.py +0 -0
  31. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/_metrics_utils.py +0 -0
  32. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/dependency_injector/__init__.py +0 -0
  33. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/dependency_injector/_base.py +0 -0
  34. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/dependency_injector/database.py +0 -0
  35. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/dependency_injector/metrics.py +0 -0
  36. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/di/__init__.py +0 -0
  37. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/di/_base.py +0 -0
  38. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/di/database.py +0 -0
  39. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/di/metrics.py +0 -0
  40. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/metrics/__init__.py +0 -0
  41. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/metrics/postgres.py +0 -0
  42. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/settings/__init__.py +0 -0
  43. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/telemetry/__init__.py +0 -0
  44. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/telemetry/instrumentations.py +0 -0
  45. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/contrib/telemetry/uow.py +0 -0
  46. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/protocols/__init__.py +0 -0
  47. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/protocols/metrics.py +0 -0
  48. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/py.typed +0 -0
  49. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/session/__init__.py +0 -0
  50. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/session/connection.py +0 -0
  51. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/session/retry.py +0 -0
  52. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/uow/__init__.py +0 -0
  53. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/uow/enums.py +0 -0
  54. {sqlalchemy_foundation_kit-0.2.0 → sqlalchemy_foundation_kit-0.3.0}/sqlalchemy_foundation_kit/uow/protocols.py +0 -0
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.0](https://github.com/bedrock-python/sqlalchemy-foundation-kit/compare/sqlalchemy-foundation-kit-v0.2.1...sqlalchemy-foundation-kit-v0.3.0) (2026-09-07)
4
+
5
+
6
+ ### Features
7
+
8
+ * stop sending jit and search_path as startup parameters ([#24](https://github.com/bedrock-python/sqlalchemy-foundation-kit/issues/24)) ([704c343](https://github.com/bedrock-python/sqlalchemy-foundation-kit/commit/704c343b932b15f0c6ee434f2fc80ab4732b0f80))
9
+
10
+ ## [0.2.1](https://github.com/bedrock-python/sqlalchemy-foundation-kit/compare/sqlalchemy-foundation-kit-v0.2.0...sqlalchemy-foundation-kit-v0.2.1) (2026-09-06)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * the defects the agents page turned up ([#21](https://github.com/bedrock-python/sqlalchemy-foundation-kit/issues/21)) ([9548c72](https://github.com/bedrock-python/sqlalchemy-foundation-kit/commit/9548c72670696dd68e0eeec1f0ae4ad896fa62d6))
16
+
3
17
  ## [0.2.0](https://github.com/bedrock-python/sqlalchemy-foundation-kit/compare/sqlalchemy-foundation-kit-v0.1.0...sqlalchemy-foundation-kit-v0.2.0) (2026-08-28)
4
18
 
5
19
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sqlalchemy-foundation-kit
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Foundation layer for SQLAlchemy-based services with UoW, session management, and observability
5
5
  Project-URL: Repository, https://github.com/bedrock-python/sqlalchemy-foundation-kit
6
6
  Project-URL: Documentation, https://bedrock-python.github.io/sqlalchemy-foundation-kit/
@@ -219,6 +219,7 @@ Classifier: Programming Language :: Python :: 3.12
219
219
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
220
220
  Classifier: Typing :: Typed
221
221
  Requires-Python: >=3.11
222
+ Requires-Dist: asyncpg<1.0.0,>=0.30.0
222
223
  Requires-Dist: pydantic<3.0.0,>=2.5.0
223
224
  Requires-Dist: sqlalchemy[asyncio]<3.0.0,>=2.0.35
224
225
  Provides-Extra: all
@@ -279,14 +280,22 @@ Description-Content-Type: text/markdown
279
280
  - **Observability** — Prometheus connection-pool metrics and OpenTelemetry tracing
280
281
  - **DI integration** — Ready-to-use providers for [`dishka`](https://github.com/reagento/dishka) and `dependency-injector`
281
282
 
282
- Only `sqlalchemy[asyncio]` and `pydantic` are required by default — everything else is an opt-in extra.
283
+ Only `sqlalchemy[asyncio]`, `pydantic` and `asyncpg` are required by default — everything else is an opt-in extra.
284
+
285
+ > [!TIP]
286
+ > **Building this with an AI assistant?** Hand it
287
+ > **[one page](https://bedrock-python.github.io/sqlalchemy-foundation-kit/agents/)** instead of the
288
+ > whole site: the public API surface, the session-lifetime and transaction-ownership rules that
289
+ > break code when they are broken, the mistakes models actually make with a Unit of Work, and a
290
+ > map of which page to fetch for the rest. Every docs page is also served as raw Markdown at its
291
+ > own URL, and a **Copy page** button at the top of each one hands it straight to a chat window.
283
292
 
284
293
  ## Key Features
285
294
 
286
295
  ✅ **Single dependency** — All foundation pieces in one place
287
296
  ✅ **Unit of Work pattern** — Transactional consistency with automatic commit/rollback
288
297
  ✅ **Connection pool management** — `AsyncSessionManager` with metrics and health checks
289
- ✅ **pgbouncer compatible** — Custom connection class for transaction mode
298
+ ✅ **PgBouncer compatible** — A startup packet PgBouncer accepts, `search_path` per transaction, unique statement names
290
299
  ✅ **Observability built-in** — Prometheus metrics + OpenTelemetry tracing
291
300
  ✅ **Type-safe configuration** — Pydantic settings with validation
292
301
  ✅ **Base ORM models** — Pre-configured `Base` with naming conventions and mixins
@@ -389,7 +398,7 @@ class PostgresConfig:
389
398
  application_name: str = "my-service"
390
399
  db_schema: str | None = None
391
400
  use_orjson_serialization: bool = True
392
- jit: str | None = "off"
401
+ jit: str | None = None
393
402
 
394
403
  def to_dsn(self) -> str:
395
404
  return f"postgresql+asyncpg://{self.connection.user}@{self.connection.host}:{self.connection.port}/{self.connection.database}"
@@ -425,12 +434,14 @@ async def main():
425
434
  user = UserDB(id=uuid4(), email="user@example.com", username="user")
426
435
  session.add(user)
427
436
  # Auto-commit on exit
437
+
438
+ await session_manager.aclose()
428
439
  ```
429
440
 
430
441
  ### 4. Unit of Work Pattern
431
442
 
432
443
  ```python
433
- from unit_of_work_kit import AsyncSQLAlchemyUnitOfWork, AsyncSQLAlchemyUowTransaction
444
+ from sqlalchemy_foundation_kit import AsyncSQLAlchemyUnitOfWork, AsyncSQLAlchemyUowTransaction
434
445
 
435
446
  # Define your transaction with repositories
436
447
  class MyTransaction(AsyncSQLAlchemyUowTransaction):
@@ -619,6 +630,8 @@ graph LR
619
630
 
620
631
  Full documentation is available at [https://bedrock-python.github.io/sqlalchemy-foundation-kit/](https://bedrock-python.github.io/sqlalchemy-foundation-kit/)
621
632
 
633
+ - [For AI agents](https://bedrock-python.github.io/sqlalchemy-foundation-kit/agents/) — the whole library on one page, for a coding assistant
634
+
622
635
  ## License
623
636
 
624
637
  This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
@@ -29,14 +29,22 @@
29
29
  - **Observability** — Prometheus connection-pool metrics and OpenTelemetry tracing
30
30
  - **DI integration** — Ready-to-use providers for [`dishka`](https://github.com/reagento/dishka) and `dependency-injector`
31
31
 
32
- Only `sqlalchemy[asyncio]` and `pydantic` are required by default — everything else is an opt-in extra.
32
+ Only `sqlalchemy[asyncio]`, `pydantic` and `asyncpg` are required by default — everything else is an opt-in extra.
33
+
34
+ > [!TIP]
35
+ > **Building this with an AI assistant?** Hand it
36
+ > **[one page](https://bedrock-python.github.io/sqlalchemy-foundation-kit/agents/)** instead of the
37
+ > whole site: the public API surface, the session-lifetime and transaction-ownership rules that
38
+ > break code when they are broken, the mistakes models actually make with a Unit of Work, and a
39
+ > map of which page to fetch for the rest. Every docs page is also served as raw Markdown at its
40
+ > own URL, and a **Copy page** button at the top of each one hands it straight to a chat window.
33
41
 
34
42
  ## Key Features
35
43
 
36
44
  ✅ **Single dependency** — All foundation pieces in one place
37
45
  ✅ **Unit of Work pattern** — Transactional consistency with automatic commit/rollback
38
46
  ✅ **Connection pool management** — `AsyncSessionManager` with metrics and health checks
39
- ✅ **pgbouncer compatible** — Custom connection class for transaction mode
47
+ ✅ **PgBouncer compatible** — A startup packet PgBouncer accepts, `search_path` per transaction, unique statement names
40
48
  ✅ **Observability built-in** — Prometheus metrics + OpenTelemetry tracing
41
49
  ✅ **Type-safe configuration** — Pydantic settings with validation
42
50
  ✅ **Base ORM models** — Pre-configured `Base` with naming conventions and mixins
@@ -139,7 +147,7 @@ class PostgresConfig:
139
147
  application_name: str = "my-service"
140
148
  db_schema: str | None = None
141
149
  use_orjson_serialization: bool = True
142
- jit: str | None = "off"
150
+ jit: str | None = None
143
151
 
144
152
  def to_dsn(self) -> str:
145
153
  return f"postgresql+asyncpg://{self.connection.user}@{self.connection.host}:{self.connection.port}/{self.connection.database}"
@@ -175,12 +183,14 @@ async def main():
175
183
  user = UserDB(id=uuid4(), email="user@example.com", username="user")
176
184
  session.add(user)
177
185
  # Auto-commit on exit
186
+
187
+ await session_manager.aclose()
178
188
  ```
179
189
 
180
190
  ### 4. Unit of Work Pattern
181
191
 
182
192
  ```python
183
- from unit_of_work_kit import AsyncSQLAlchemyUnitOfWork, AsyncSQLAlchemyUowTransaction
193
+ from sqlalchemy_foundation_kit import AsyncSQLAlchemyUnitOfWork, AsyncSQLAlchemyUowTransaction
184
194
 
185
195
  # Define your transaction with repositories
186
196
  class MyTransaction(AsyncSQLAlchemyUowTransaction):
@@ -369,6 +379,8 @@ graph LR
369
379
 
370
380
  Full documentation is available at [https://bedrock-python.github.io/sqlalchemy-foundation-kit/](https://bedrock-python.github.io/sqlalchemy-foundation-kit/)
371
381
 
382
+ - [For AI agents](https://bedrock-python.github.io/sqlalchemy-foundation-kit/agents/) — the whole library on one page, for a coding assistant
383
+
372
384
  ## License
373
385
 
374
386
  This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
@@ -31,6 +31,7 @@ classifiers = [
31
31
  dependencies = [
32
32
  "sqlalchemy[asyncio]>=2.0.35,<3.0.0",
33
33
  "pydantic>=2.5.0,<3.0.0",
34
+ "asyncpg>=0.30.0,<1.0.0",
34
35
  ]
35
36
 
36
37
  [project.urls]
@@ -130,6 +131,10 @@ warn_unreachable = true
130
131
  [tool.ruff]
131
132
  line-length = 120
132
133
  target-version = "py311"
134
+ # The Markdown guides hold fragments -- a keyword argument on its own, a body
135
+ # without its def -- that read as code to a formatter and come out as
136
+ # something else. ruff 0.16 formats fenced Python in Markdown by default.
137
+ extend-exclude = ["*.md"]
133
138
 
134
139
  [tool.ruff.lint]
135
140
  select = ["F", "E", "W", "I", "B", "N", "S", "C4", "DTZ", "SIM", "TRY", "PERF", "RUF", "UP", "ANN", "T20", "PTH", "PLC", "PLE", "PLW"]
@@ -64,7 +64,7 @@ from .uow import (
64
64
  try:
65
65
  __version__ = version("sqlalchemy-foundation-kit")
66
66
  except PackageNotFoundError: # pragma: no cover
67
- __version__ = "0.2.0"
67
+ __version__ = "0.3.0"
68
68
 
69
69
  __all__ = [ # noqa: RUF022
70
70
  # Base ORM
@@ -0,0 +1 @@
1
+ __version__ = "0.3.0"
@@ -11,7 +11,7 @@ def require_optional(module_name: str, extra_name: str) -> types.ModuleType:
11
11
 
12
12
  Args:
13
13
  module_name: Name of the module to import (e.g., "orjson", "opentelemetry").
14
- extra_name: Name of the pip extra that provides this dependency (e.g., "json", "telemetry").
14
+ extra_name: Name of the pip extra that provides this dependency (e.g., "orjson", "telemetry").
15
15
 
16
16
  Returns:
17
17
  The imported module.
@@ -20,7 +20,7 @@ def require_optional(module_name: str, extra_name: str) -> types.ModuleType:
20
20
  ImportError: If the module is not installed, with installation instructions.
21
21
 
22
22
  Examples:
23
- >>> orjson = require_optional("orjson", "json")
23
+ >>> orjson = require_optional("orjson", "orjson")
24
24
  >>> from opentelemetry import trace
25
25
  # or
26
26
  >>> otel = require_optional("opentelemetry", "telemetry")
@@ -57,7 +57,7 @@ def _json_serializer(obj: object) -> str:
57
57
  >>> _json_serializer({"key": "value"})
58
58
  '{"key":"value"}'
59
59
  """
60
- orjson = require_optional("orjson", "json")
60
+ orjson = require_optional("orjson", "orjson")
61
61
 
62
62
  try:
63
63
  return orjson.dumps(obj, default=_default_json_encoder).decode("utf-8") # type: ignore[no-any-return]
@@ -82,7 +82,7 @@ def configure_orjson_serialization() -> dict[str, object]:
82
82
  >>> "json_deserializer" in config
83
83
  True
84
84
  """
85
- orjson = require_optional("orjson", "json")
85
+ orjson = require_optional("orjson", "orjson")
86
86
 
87
87
  return {
88
88
  "json_serializer": _json_serializer,
@@ -131,9 +131,9 @@ class PostgresSettingsProtocol(Protocol):
131
131
  pool: Connection pool settings.
132
132
  query: Query execution and transaction settings.
133
133
  application_name: Application identifier for connections.
134
- db_schema: Optional PostgreSQL schema name.
134
+ db_schema: Optional PostgreSQL ``search_path``, applied to every transaction.
135
135
  use_orjson_serialization: Enable orjson for JSON operations.
136
- jit: JIT compilation setting (PgBouncer compatibility).
136
+ jit: JIT compilation setting, sent as a startup parameter only when not ``None``.
137
137
 
138
138
  Examples:
139
139
  Implementing the protocol:
@@ -144,7 +144,7 @@ class PostgresSettingsProtocol(Protocol):
144
144
  ... application_name: str = "my-app"
145
145
  ... db_schema: str | None = None
146
146
  ... use_orjson_serialization: bool = True
147
- ... jit: str | None = "off"
147
+ ... jit: str | None = None
148
148
  ...
149
149
  ... def to_dsn(self) -> str:
150
150
  ... return f"postgresql://{self.connection.user}@{self.connection.host}..."
@@ -0,0 +1,59 @@
1
+ """Shared dependency-injector dependency helpers.
2
+
3
+ Centralizes the dependency-injector import boilerplate and availability check so each
4
+ module doesn't have to repeat it.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any, NoReturn
10
+
11
+
12
+ def check_dependency_injector() -> None:
13
+ """Raise ImportError if dependency-injector is not installed.
14
+
15
+ Raises:
16
+ ImportError: If dependency-injector is not available.
17
+ """
18
+ if not HAS_DEPENDENCY_INJECTOR:
19
+ raise ImportError(
20
+ "dependency-injector is required for containers. "
21
+ "Install it with: pip install 'sqlalchemy-foundation-kit[dependency-injector]'"
22
+ )
23
+
24
+
25
+ class _MissingDependencyInjector:
26
+ """Stand-in for a dependency-injector name, raising the intended ImportError on first use.
27
+
28
+ ``BaseDIContainer`` subclasses ``containers.DeclarativeContainer`` and the container
29
+ modules build ``providers.*`` in their class bodies, which runs before any check a
30
+ base class could perform. Without this, importing them without dependency-injector
31
+ installed fails with ``AttributeError: 'NoneType' object has no attribute
32
+ 'DeclarativeContainer'`` instead of the message that says what to install.
33
+ """
34
+
35
+ def __getattr__(self, name: str) -> NoReturn:
36
+ check_dependency_injector()
37
+ raise AttributeError(name) # pragma: no cover - unreachable, check raises
38
+
39
+ def __call__(self, *args: Any, **kwargs: Any) -> NoReturn:
40
+ check_dependency_injector()
41
+ raise TypeError("dependency-injector is not installed") # pragma: no cover - unreachable
42
+
43
+
44
+ try:
45
+ from dependency_injector import containers, providers
46
+
47
+ HAS_DEPENDENCY_INJECTOR = True
48
+ except ImportError:
49
+ HAS_DEPENDENCY_INJECTOR = False
50
+ containers = _MissingDependencyInjector() # type: ignore[misc,assignment]
51
+ providers = _MissingDependencyInjector() # type: ignore[misc,assignment]
52
+
53
+
54
+ __all__ = [
55
+ "HAS_DEPENDENCY_INJECTOR",
56
+ "check_dependency_injector",
57
+ "containers",
58
+ "providers",
59
+ ]
@@ -0,0 +1,53 @@
1
+ """Shared dishka dependency helpers.
2
+
3
+ Centralizes the dishka import boilerplate and availability check so each
4
+ provider module doesn't have to repeat it.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any, NoReturn
10
+
11
+
12
+ def check_dishka() -> None:
13
+ """Raise ImportError if dishka is not installed.
14
+
15
+ Raises:
16
+ ImportError: If dishka is not available.
17
+ """
18
+ if not HAS_DISHKA:
19
+ raise ImportError(
20
+ "dishka is required for providers. Install it with: pip install 'sqlalchemy-foundation-kit[dishka]'"
21
+ )
22
+
23
+
24
+ class _MissingDishka:
25
+ """Stand-in for a dishka name, raising the intended ImportError on first use.
26
+
27
+ Provider modules read ``Scope.APP`` and apply ``@provide`` in their class bodies,
28
+ which runs before any check a base class could perform. Without this, importing
29
+ them without dishka installed fails with ``AttributeError: 'NoneType' object has
30
+ no attribute 'APP'`` instead of the message that says what to install.
31
+ """
32
+
33
+ def __getattr__(self, name: str) -> NoReturn:
34
+ check_dishka()
35
+ raise AttributeError(name) # pragma: no cover - unreachable, check_dishka raises
36
+
37
+ def __call__(self, *args: Any, **kwargs: Any) -> NoReturn:
38
+ check_dishka()
39
+ raise TypeError("dishka is not installed") # pragma: no cover - unreachable
40
+
41
+
42
+ try:
43
+ from dishka import Provider, Scope, provide
44
+
45
+ HAS_DISHKA = True
46
+ except ImportError:
47
+ HAS_DISHKA = False
48
+ Provider = object # type: ignore[misc,assignment]
49
+ Scope = _MissingDishka() # type: ignore[misc,assignment]
50
+ provide = _MissingDishka() # type: ignore[misc,assignment]
51
+
52
+
53
+ __all__ = ["HAS_DISHKA", "Provider", "Scope", "check_dishka", "provide"]
@@ -98,9 +98,12 @@ class BasePostgresConfig(BaseSettings):
98
98
  pool: Connection pool configuration (size, overflow, timeouts).
99
99
  query: Query execution settings (echo, caching, isolation level).
100
100
  application_name: Application name for connection identification.
101
- db_schema: Optional PostgreSQL schema name.
101
+ db_schema: Optional PostgreSQL ``search_path`` — a schema, or a comma-separated
102
+ list — applied to every transaction with ``SET LOCAL`` semantics.
102
103
  use_orjson_serialization: Use orjson for JSON serialization (requires orjson).
103
- jit: JIT compilation setting (off/on) for PgBouncer compatibility.
104
+ jit: JIT compilation setting (off/on), sent as a startup parameter only when set.
105
+ ``None`` (the default) sends nothing and leaves the server's own setting; a
106
+ transaction-mode PgBouncer rejects the parameter unless it tracks it.
104
107
  metrics_enabled: Enable connection pool metrics collection.
105
108
 
106
109
  Examples:
@@ -125,12 +128,18 @@ class BasePostgresConfig(BaseSettings):
125
128
 
126
129
  # Top-level settings
127
130
  application_name: str = Field(description="Application name for PostgreSQL")
128
- db_schema: str | None = Field(default=None, description="PostgreSQL schema name")
131
+ db_schema: str | None = Field(
132
+ default=None,
133
+ description="PostgreSQL search_path (a schema or a comma-separated list), applied to every transaction",
134
+ )
129
135
  use_orjson_serialization: bool = Field(
130
136
  default=True,
131
137
  description="Use orjson for JSON serialization (requires orjson installed)",
132
138
  )
133
- jit: PostgresJit | None = Field(default="off", description="JIT setting (off/on)")
139
+ jit: PostgresJit | None = Field(
140
+ default=None,
141
+ description="JIT setting (off/on), sent as a startup parameter only when set",
142
+ )
134
143
  metrics_enabled: bool = Field(default=False, description="Enable PostgreSQL metrics")
135
144
 
136
145
  def __repr__(self) -> str:
@@ -77,6 +77,7 @@ class AsyncSessionManagerBuilder(Generic[SessionT]):
77
77
  self._metrics: PostgresMetricsProtocol | None = None
78
78
  self._on_engine_created: Callable[[AsyncEngine], None] | None = None
79
79
  self._dispose_timeout: float | None = None
80
+ self._search_path: str | None = None
80
81
  self._extra_kwargs: dict[str, object] = {}
81
82
 
82
83
  def with_echo(self, echo: bool = True) -> AsyncSessionManagerBuilder[SessionT]:
@@ -258,6 +259,22 @@ class AsyncSessionManagerBuilder(Generic[SessionT]):
258
259
  self._dispose_timeout = timeout
259
260
  return self
260
261
 
262
+ def with_search_path(self, search_path: str) -> AsyncSessionManagerBuilder[SessionT]:
263
+ """Apply a PostgreSQL ``search_path`` to every transaction.
264
+
265
+ Issued with ``SET LOCAL`` semantics at the start of each transaction — the one
266
+ scope a transaction-mode pooler such as PgBouncer honours. See
267
+ :func:`~sqlalchemy_foundation_kit.session.manager.attach_search_path`.
268
+
269
+ Args:
270
+ search_path: A schema, or a comma-separated list such as ``"tenant_7, public"``.
271
+
272
+ Returns:
273
+ Self for method chaining.
274
+ """
275
+ self._search_path = search_path
276
+ return self
277
+
261
278
  def build(self) -> AsyncSessionManager[SessionT]:
262
279
  """Build AsyncSessionManager instance with configured parameters.
263
280
 
@@ -284,6 +301,7 @@ class AsyncSessionManagerBuilder(Generic[SessionT]):
284
301
  "use_orjson": self._use_orjson,
285
302
  "metrics": self._metrics,
286
303
  "on_engine_created": self._on_engine_created,
304
+ "search_path": self._search_path,
287
305
  }
288
306
  if self._dispose_timeout is not None:
289
307
  kwargs["dispose_timeout"] = self._dispose_timeout
@@ -31,6 +31,13 @@ def create_async_session_manager(
31
31
  ) -> AsyncSessionManager[AsyncSession]:
32
32
  """Create async session manager with PostgreSQL-specific configuration.
33
33
 
34
+ The startup packet carries ``application_name`` and, only when the config sets it,
35
+ ``jit``; nothing else, because a transaction-mode pooler such as PgBouncer rejects
36
+ startup parameters it does not track (``unsupported startup parameter: jit``) or,
37
+ with ``ignore_startup_parameters``, silently drops them. ``db_schema`` therefore
38
+ travels as the manager's ``search_path`` and is applied with ``SET LOCAL`` semantics
39
+ at the start of every transaction, which is the one scope such a pooler honours.
40
+
34
41
  Args:
35
42
  postgres_config: PostgreSQL configuration implementing PostgresSettingsProtocol.
36
43
  application_name: Optional custom application name. If None, uses postgres_config.application_name.
@@ -41,7 +48,8 @@ def create_async_session_manager(
41
48
  which provides pgbouncer transaction-mode compatibility.
42
49
  extra_server_settings: Additional PostgreSQL ``server_settings`` to merge with defaults
43
50
  (e.g., ``{"statement_timeout": "30000", "timezone": "UTC"}``). User-provided keys
44
- override library defaults.
51
+ override library defaults. These are startup parameters: through PgBouncer only
52
+ the ones it tracks arrive (``track_extra_parameters``).
45
53
  extra_connect_args: Additional asyncpg ``connect_args`` to merge with defaults
46
54
  (e.g., ``{"command_timeout": 60}``). User-provided keys override library defaults.
47
55
  **kwargs: Additional keyword arguments passed to AsyncSessionManager.
@@ -73,11 +81,11 @@ def create_async_session_manager(
73
81
  """
74
82
  app_name = application_name or postgres_config.application_name
75
83
 
76
- # Build server settings with optional overrides
84
+ # Build server settings with optional overrides. These are startup parameters, so
85
+ # search_path is deliberately not among them -- see the docstring.
77
86
  server_settings: dict[str, str] = {
78
87
  "application_name": app_name,
79
88
  **({"jit": postgres_config.jit} if postgres_config.jit is not None else {}),
80
- **({"search_path": postgres_config.db_schema} if postgres_config.db_schema is not None else {}),
81
89
  **(extra_server_settings or {}),
82
90
  }
83
91
 
@@ -100,5 +108,6 @@ def create_async_session_manager(
100
108
  use_orjson=postgres_config.use_orjson_serialization,
101
109
  metrics=metrics,
102
110
  on_engine_created=on_engine_created,
111
+ search_path=postgres_config.db_schema,
103
112
  **kwargs,
104
113
  )
@@ -1,5 +1,7 @@
1
1
  """PostgreSQL advisory locks (async)."""
2
2
 
3
+ import hashlib
4
+
3
5
  from sqlalchemy import text
4
6
  from sqlalchemy.ext.asyncio import AsyncSession
5
7
 
@@ -15,6 +17,10 @@ async def try_advisory_xact_lock(session: AsyncSession, key: str | int) -> bool:
15
17
  at transaction end. String keys are hashed to integers. The key is then
16
18
  truncated to signed 64-bit as Postgres expects.
17
19
 
20
+ A string key produces the same lock in every process, on every host and in
21
+ every release of the library, so two replicas of a service asking for
22
+ ``"nightly-rollup"`` contend for one lock.
23
+
18
24
  Args:
19
25
  session: SQLAlchemy AsyncSession within an active transaction.
20
26
  key: Lock identifier (string or integer). Strings are hashed to integers.
@@ -31,7 +37,7 @@ async def try_advisory_xact_lock(session: AsyncSession, key: str | int) -> bool:
31
37
  ... await session.commit()
32
38
  """
33
39
  # Convert string keys to integers via hashing
34
- int_key = hash(key) if isinstance(key, str) else key
40
+ int_key = _hash_lock_key(key) if isinstance(key, str) else key
35
41
 
36
42
  result = await session.execute(
37
43
  text("SELECT pg_try_advisory_xact_lock(:k)"),
@@ -40,6 +46,28 @@ async def try_advisory_xact_lock(session: AsyncSession, key: str | int) -> bool:
40
46
  return bool(result.scalar())
41
47
 
42
48
 
49
+ def _hash_lock_key(key: str) -> int:
50
+ """Hash a string lock key into the PostgreSQL bigint range, reproducibly.
51
+
52
+ Python's built-in ``hash()`` is unusable here: string hashing is salted per
53
+ interpreter, so the same key becomes a different lock in every process and two
54
+ replicas of the same service lock nothing against each other. BLAKE2b is
55
+ deterministic, so the key is stable across processes, hosts and restarts.
56
+
57
+ Args:
58
+ key: Lock identifier.
59
+
60
+ Returns:
61
+ A signed 64-bit integer suitable for ``pg_try_advisory_xact_lock``.
62
+
63
+ Examples:
64
+ >>> _hash_lock_key("nightly-rollup") == _hash_lock_key("nightly-rollup")
65
+ True
66
+ """
67
+ digest = hashlib.blake2b(key.encode("utf-8"), digest_size=8).digest()
68
+ return int.from_bytes(digest, byteorder="big", signed=True)
69
+
70
+
43
71
  def _to_signed64(key: int) -> int:
44
72
  """Wrap integer to PostgreSQL signed 64-bit bigint range.
45
73
 
@@ -10,7 +10,7 @@ from contextlib import asynccontextmanager
10
10
  from types import TracebackType
11
11
  from typing import TYPE_CHECKING, Any, Generic, cast
12
12
 
13
- from sqlalchemy import event
13
+ from sqlalchemy import event, text
14
14
  from sqlalchemy.exc import TimeoutError as SATimeoutError
15
15
  from sqlalchemy.ext.asyncio import (
16
16
  AsyncEngine,
@@ -23,6 +23,8 @@ from .._typing import SessionT
23
23
  from ..base import build_engine_kwargs, resolve_pool_class
24
24
 
25
25
  if TYPE_CHECKING:
26
+ from sqlalchemy.engine import Connection
27
+
26
28
  from ..config import PoolSettingsProtocol
27
29
  from ..protocols import PostgresMetricsProtocol
28
30
 
@@ -100,6 +102,35 @@ def attach_metrics(engine: AsyncEngine, metrics: PostgresMetricsProtocol) -> Non
100
102
  event.listen(engine.sync_engine, "handle_error", on_error)
101
103
 
102
104
 
105
+ def attach_search_path(engine: AsyncEngine, search_path: str) -> None:
106
+ """Apply ``search_path`` to every transaction the engine begins.
107
+
108
+ Registers a ``begin`` listener that issues ``set_config('search_path', …, true)`` —
109
+ the function form of ``SET LOCAL``, with the value as a bind parameter — as the first
110
+ statement of each transaction, so the setting lives exactly as long as the transaction
111
+ does. That is the one scope a transaction-mode pooler such as PgBouncer honours: a
112
+ startup parameter is rejected or dropped before it reaches PostgreSQL, and a plain
113
+ ``SET`` on a server connection leaks to whichever client is handed it next.
114
+
115
+ Under the asyncpg adapter ``BEGIN`` is sent lazily with the first statement, so the
116
+ ``set_config`` call lands inside the transaction rather than ahead of it. Every
117
+ transaction is covered — a session's autobegin, ``session.begin()``, a raw
118
+ ``engine.connect()``, the one that follows a ``commit()`` — but a statement run with
119
+ ``isolation_level="AUTOCOMMIT"`` begins none and runs with the server's default.
120
+
121
+ Args:
122
+ engine: SQLAlchemy ``AsyncEngine`` to attach the listener to.
123
+ search_path: Value for ``search_path`` — a schema, or a comma-separated list such
124
+ as ``"tenant_7, public"``.
125
+ """
126
+ statement = text("SELECT set_config('search_path', :search_path, true)")
127
+
128
+ def on_begin(conn: Connection) -> None:
129
+ conn.execute(statement, {"search_path": search_path})
130
+
131
+ event.listen(engine.sync_engine, "begin", on_begin)
132
+
133
+
103
134
  class AsyncSessionManager(Generic[SessionT]):
104
135
  """Manages async database sessions with configurable connection pooling.
105
136
 
@@ -124,6 +155,7 @@ class AsyncSessionManager(Generic[SessionT]):
124
155
  metrics: PostgresMetricsProtocol | None = None,
125
156
  on_engine_created: Callable[[AsyncEngine], None] | None = None,
126
157
  dispose_timeout: float = DEFAULT_DISPOSE_TIMEOUT_SECONDS,
158
+ search_path: str | None = None,
127
159
  **kwargs: object,
128
160
  ) -> None:
129
161
  """Initialize session manager with direct configuration.
@@ -145,6 +177,10 @@ class AsyncSessionManager(Generic[SessionT]):
145
177
  dispose_timeout: Maximum seconds to wait for engine disposal in :meth:`aclose`
146
178
  (default: 30.0). Lower this in tests or short-lived environments; raise it
147
179
  if you have long-running transactions that need more time to settle.
180
+ search_path: PostgreSQL ``search_path`` applied to every transaction with
181
+ ``SET LOCAL`` semantics, which is what survives a transaction-mode pooler
182
+ (default: None — nothing is sent, the server default applies). See
183
+ :func:`attach_search_path`.
148
184
  **kwargs: Additional keyword arguments for ``create_async_engine``.
149
185
  """
150
186
  self._closed = False
@@ -174,6 +210,9 @@ class AsyncSessionManager(Generic[SessionT]):
174
210
  if metrics:
175
211
  attach_metrics(self._engine, metrics)
176
212
 
213
+ if search_path is not None:
214
+ attach_search_path(self._engine, search_path)
215
+
177
216
  if on_engine_created is not None:
178
217
  on_engine_created(self._engine)
179
218
 
@@ -238,15 +277,18 @@ class AsyncSessionManager(Generic[SessionT]):
238
277
  """Get a new database session with automatic transaction management.
239
278
 
240
279
  Args:
241
- isolation_level: Optional isolation level for the transaction.
280
+ isolation_level: Optional isolation level for the transaction, in the
281
+ PostgreSQL spelling (``"SERIALIZABLE"``, ``"REPEATABLE READ"``, …).
282
+ It is applied to the connection this transaction runs on, so it
283
+ affects only this transaction and not the engine.
242
284
 
243
285
  Yields:
244
286
  Managed async session with active transaction.
245
287
  """
246
288
  self._ensure_not_closed()
247
- options = {"isolation_level": isolation_level} if isolation_level else {}
248
- async with (
249
- self._session_maker(execution_options=options) as session,
250
- session.begin(),
251
- ):
289
+ async with self._session_maker() as session, session.begin():
290
+ if isolation_level is not None:
291
+ # begin() has not provisioned a connection yet, so this call is the one
292
+ # that checks it out -- the only moment the level can still be set.
293
+ await session.connection(execution_options={"isolation_level": isolation_level})
252
294
  yield session
@@ -74,10 +74,15 @@ async def apply_isolation_level(
74
74
  ) -> None:
75
75
  """Apply isolation level to an async session's connection.
76
76
 
77
- **Implementation Detail**:
78
- We use ``run_sync()`` because SQLAlchemy's ``execution_options()`` is a
79
- synchronous method that configures the underlying DBAPI connection object.
80
- We must bridge from async context to sync method via ``run_sync()``.
77
+ The level has to reach the connection *before* the connection begins its
78
+ transaction PostgreSQL cannot change the isolation level of a transaction that
79
+ is already running, and SQLAlchemy raises ``InvalidRequestError`` if you try. So
80
+ the level is handed to the ``session.connection()`` call that checks the
81
+ connection out, which applies it and only then begins.
82
+
83
+ Call this on a session that has not yet talked to the database. It starts the
84
+ session's transaction as a side effect, so a caller that wants to own the
85
+ transaction should join the one already open rather than call ``session.begin()``.
81
86
 
82
87
  This is a DRY utility to eliminate duplication of isolation level application
83
88
  logic across ``transaction()``, ``managed_session()``, and ``query()`` methods.
@@ -103,9 +108,32 @@ async def apply_isolation_level(
103
108
  """
104
109
  normalized = normalize_isolation_level(isolation_level)
105
110
  if normalized is not None:
106
- conn = await session.connection()
107
- # run_sync bridges async → sync for DBAPI-level configuration
108
- await conn.run_sync(lambda c: c.execution_options(isolation_level=normalized))
111
+ await session.connection(execution_options={"isolation_level": normalized})
112
+
113
+
114
+ @asynccontextmanager
115
+ async def _owned_transaction(session: AsyncSession) -> AsyncIterator[None]:
116
+ """Run a block inside a transaction, committing on success and rolling back on failure.
117
+
118
+ ``session.begin()`` refuses to start a second transaction, and applying an isolation
119
+ level has to check a connection out — which begins one. So the transaction is only
120
+ started here when the session does not already have one, and the commit/rollback is
121
+ driven explicitly instead of by the ``session.begin()`` context manager.
122
+ """
123
+ if not session.in_transaction():
124
+ await session.begin()
125
+
126
+ try:
127
+ yield
128
+ except BaseException:
129
+ await session.rollback()
130
+ raise
131
+
132
+ try:
133
+ await session.commit()
134
+ except BaseException:
135
+ await session.rollback()
136
+ raise
109
137
 
110
138
 
111
139
  class AsyncSQLAlchemyUowTransaction(AsyncUowTransaction):
@@ -181,13 +209,14 @@ class PostgresAdvisoryLockMixin:
181
209
 
182
210
  session: AsyncSession # Type annotation for protocol compliance
183
211
 
184
- async def try_advisory_lock(self, key: int) -> bool:
212
+ async def try_advisory_lock(self, key: str | int) -> bool:
185
213
  """Acquire a Postgres transaction-scoped advisory lock.
186
214
 
187
215
  Delegates to :func:`try_advisory_xact_lock` for actual locking logic.
188
216
 
189
217
  Args:
190
- key: Integer lock key.
218
+ key: Lock key. An integer is used as-is; a string is hashed to one
219
+ reproducibly, so the same string is the same lock in every process.
191
220
 
192
221
  Returns:
193
222
  True if lock was acquired, False if already held by another session.
@@ -301,7 +330,7 @@ class AsyncSQLAlchemyUnitOfWork(AsyncUnitOfWork[T], Generic[T]):
301
330
  if flush_before_commit is None:
302
331
  flush_before_commit = self._flush_before_commit
303
332
 
304
- async with self.open_session(isolation_level) as session, session.begin():
333
+ async with self.open_session(isolation_level) as session, _owned_transaction(session):
305
334
  uow = self._transaction_factory(session)
306
335
  yield uow
307
336
  if flush_before_commit:
@@ -390,8 +419,10 @@ class AsyncSQLAlchemyUnitOfWork(AsyncUnitOfWork[T], Generic[T]):
390
419
  rollback when the session closes.
391
420
  """
392
421
  async with self.open_session(isolation_level) as session:
393
- # Start transaction WITHOUT context manager - no auto-commit
394
- await session.begin()
422
+ # Start transaction WITHOUT context manager - no auto-commit.
423
+ # An isolation level, if one was asked for, has already begun it.
424
+ if not session.in_transaction():
425
+ await session.begin()
395
426
  try:
396
427
  uow = self._transaction_factory(session)
397
428
  yield uow, session
@@ -1 +0,0 @@
1
- __version__ = "0.2.0" # x-release-please-version
@@ -1,37 +0,0 @@
1
- """Shared dependency-injector dependency helpers.
2
-
3
- Centralizes the dependency-injector import boilerplate and availability check so each
4
- module doesn't have to repeat it.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- try:
10
- from dependency_injector import containers, providers
11
-
12
- HAS_DEPENDENCY_INJECTOR = True
13
- except ImportError:
14
- HAS_DEPENDENCY_INJECTOR = False
15
- containers = None # type: ignore[misc,assignment]
16
- providers = None # type: ignore[misc,assignment]
17
-
18
-
19
- def check_dependency_injector() -> None:
20
- """Raise ImportError if dependency-injector is not installed.
21
-
22
- Raises:
23
- ImportError: If dependency-injector is not available.
24
- """
25
- if not HAS_DEPENDENCY_INJECTOR:
26
- raise ImportError(
27
- "dependency-injector is required for containers. "
28
- "Install it with: pip install 'sqlalchemy-foundation-kit[dependency-injector]'"
29
- )
30
-
31
-
32
- __all__ = [
33
- "HAS_DEPENDENCY_INJECTOR",
34
- "check_dependency_injector",
35
- "containers",
36
- "providers",
37
- ]
@@ -1,32 +0,0 @@
1
- """Shared dishka dependency helpers.
2
-
3
- Centralizes the dishka import boilerplate and availability check so each
4
- provider module doesn't have to repeat it.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- try:
10
- from dishka import Provider, Scope, provide
11
-
12
- HAS_DISHKA = True
13
- except ImportError:
14
- HAS_DISHKA = False
15
- Provider = object # type: ignore[misc,assignment]
16
- Scope = None # type: ignore[misc,assignment]
17
- provide = None # type: ignore[misc,assignment]
18
-
19
-
20
- def check_dishka() -> None:
21
- """Raise ImportError if dishka is not installed.
22
-
23
- Raises:
24
- ImportError: If dishka is not available.
25
- """
26
- if not HAS_DISHKA:
27
- raise ImportError(
28
- "dishka is required for providers. Install it with: pip install 'sqlalchemy-foundation-kit[dishka]'"
29
- )
30
-
31
-
32
- __all__ = ["HAS_DISHKA", "Provider", "Scope", "check_dishka", "provide"]