voltwire-db-session 0.0.1__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.
@@ -0,0 +1,221 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ # Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ # poetry.lock
109
+ # poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ # pdm.lock
116
+ # pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ # pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # Redis
135
+ *.rdb
136
+ *.aof
137
+ *.pid
138
+
139
+ # RabbitMQ
140
+ mnesia/
141
+ rabbitmq/
142
+ rabbitmq-data/
143
+
144
+ # ActiveMQ
145
+ activemq-data/
146
+
147
+ # SageMath parsed files
148
+ *.sage.py
149
+
150
+ # Environments
151
+ .env
152
+ .envrc
153
+ .venv
154
+ env/
155
+ venv/
156
+ ENV/
157
+ env.bak/
158
+ venv.bak/
159
+
160
+ # Spyder project settings
161
+ .spyderproject
162
+ .spyproject
163
+
164
+ # Rope project settings
165
+ .ropeproject
166
+
167
+ # mkdocs documentation
168
+ /site
169
+
170
+ # mypy
171
+ .mypy_cache/
172
+ .dmypy.json
173
+ dmypy.json
174
+
175
+ # Pyre type checker
176
+ .pyre/
177
+
178
+ # pytype static type analyzer
179
+ .pytype/
180
+
181
+ # Cython debug symbols
182
+ cython_debug/
183
+
184
+ # PyCharm
185
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
186
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
187
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
188
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
189
+ .idea/
190
+
191
+ # Abstra
192
+ # Abstra is an AI-powered process automation framework.
193
+ # Ignore directories containing user credentials, local state, and settings.
194
+ # Learn more at https://abstra.io/docs
195
+ .abstra/
196
+
197
+ # Visual Studio Code
198
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
199
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
200
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
201
+ # you could uncomment the following to ignore the entire vscode folder
202
+ # .vscode/
203
+ # Temporary file for partial code execution
204
+ tempCodeRunnerFile.py
205
+
206
+ # Ruff stuff:
207
+ .ruff_cache/
208
+
209
+ # PyPI configuration file
210
+ .pypirc
211
+
212
+ # Marimo
213
+ marimo/_static/
214
+ marimo/_lsp/
215
+ __marimo__/
216
+
217
+ # Streamlit
218
+ .streamlit/secrets.toml
219
+
220
+ # Local tool state
221
+ .omc/
@@ -0,0 +1,170 @@
1
+ Metadata-Version: 2.5
2
+ Name: voltwire-db-session
3
+ Version: 0.0.1
4
+ Summary: A configurable DB session factory
5
+ Author-email: Hermann Steidel <hsteidel.software@gmail.com>
6
+ License-Expression: MIT
7
+ Requires-Python: <4.0,>=3.13
8
+ Requires-Dist: pydantic-settings<3,>=2.3
9
+ Requires-Dist: pydantic<3,>=2.9
10
+ Requires-Dist: sqlalchemy<3,>=2.0
11
+ Description-Content-Type: text/markdown
12
+
13
+ <img src="https://raw.githubusercontent.com/hsteidel/voltwire/main/assets/icons/db-session.svg" alt="" width="56" height="56" align="left">
14
+
15
+ # voltwire-db-session
16
+
17
+ A configurable PostgreSQL session factory for Python applications. Wraps SQLAlchemy connection pool setup and pydantic-settings configuration into a single reusable package — install it, point it at your `.env`, and get sessions.
18
+
19
+ ## ORM-agnostic core
20
+
21
+ `DatabaseSessionFactory`, `RODatabaseSessionFactory`, and `Session` (exported from `voltwire.db.session`) are `Protocol`s, not concrete classes. `TransactionContext` and `DatabaseAutoConfiguration` are written entirely against these interfaces and never import `sqlalchemy` — only `voltwire.db.session.backends.sqlalchemy` does. Today that's the only backend (`SqlAlchemyDatabaseSessionFactory`/`SqlAlchemyRODatabaseSessionFactory`, built via `build_session_factory`/`build_ro_session_factory`), but a future ORM backend only needs to satisfy the same `Protocol`s — no changes required to `TransactionContext`, `DatabaseAutoConfiguration`, or downstream packages like `voltwire-fastapi-db-txs`, which already depend only on the abstraction.
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ pip install voltwire-db-session
27
+ # or with Poetry:
28
+ poetry add voltwire-db-session
29
+ ```
30
+
31
+ A PostgreSQL driver is **not** included — install whichever you prefer alongside it:
32
+
33
+ ```bash
34
+ pip install psycopg2-binary # most common
35
+ pip install psycopg # psycopg3
36
+ ```
37
+
38
+ ## Quickstart
39
+
40
+ ```python
41
+ from voltwire.db.session import DatabaseSettings, build_session_factory
42
+
43
+ settings = DatabaseSettings() # reads DB_* vars from .env
44
+ factory = build_session_factory(settings)
45
+
46
+ session = factory.get_session()
47
+ try:
48
+ result = session.execute(...)
49
+ session.commit()
50
+ finally:
51
+ session.close()
52
+
53
+ # On app shutdown
54
+ factory.close()
55
+ ```
56
+
57
+ ## Configuration
58
+
59
+ All settings are loaded from environment variables with a `DB_` prefix. By default the library reads from a `.env` file in the working directory.
60
+
61
+ ### Environment variables
62
+
63
+ | Variable | Default | Description |
64
+ |-----------------------|---------------|--------------------------------------------------|
65
+ | `DB_HOST` | `localhost` | Primary database host |
66
+ | `DB_PORT` | `5432` | Database port |
67
+ | `DB_DATABASE` | `postgres` | Database name |
68
+ | `DB_USERNAME` | `postgres` | Database username |
69
+ | `DB_PASSWORD` | `postgres` | Database password |
70
+ | `DB_SCHEMA_NAME` | `public` | PostgreSQL schema (used for `search_path`) |
71
+ | `DB_DRIVER` | `psycopg2` | SQLAlchemy driver name |
72
+ | `DB_RO_HOST` | *(unset)* | Read-only replica host; falls back to `DB_HOST` |
73
+ | `DB_POOL_SIZE` | `10` | Minimum connections in pool |
74
+ | `DB_MAX_POOL_SIZE` | `20` | Maximum connections in pool |
75
+ | `DB_POOL_TIMEOUT` | `30` | Seconds to wait for a connection from pool |
76
+ | `DB_POOL_RECYCLE` | `299` | Recycle connections after this many seconds |
77
+ | `DB_APPLICATION_NAME` | `app` | Application name reported to PostgreSQL |
78
+
79
+ ### Choosing your env file
80
+
81
+ ```python
82
+ # Standard .env (default)
83
+ settings = DatabaseSettings()
84
+
85
+ # Custom env file — e.g. .env.local, .env.production
86
+ settings = DatabaseSettings.from_env(".env.local")
87
+
88
+ # No file — reads only from real environment variables
89
+ settings = DatabaseSettings.from_env(None)
90
+
91
+ # No file, with inline overrides
92
+ settings = DatabaseSettings.from_env(None, host="db.internal", database="myapp")
93
+ ```
94
+
95
+ ### Using a different driver
96
+
97
+ ```python
98
+ # psycopg3
99
+ settings = DatabaseSettings.from_env(".env", driver="psycopg")
100
+
101
+ # or via env var
102
+ # DB_DRIVER=psycopg
103
+ ```
104
+
105
+ The `driver` value is used as the SQLAlchemy URL scheme: `postgresql+{driver}://...`. The corresponding package must be installed in your environment.
106
+
107
+ ## Read-only replica
108
+
109
+ ```python
110
+ from voltwire.db.session import DatabaseSettings, build_ro_session_factory
111
+
112
+ settings = DatabaseSettings() # set DB_RO_HOST to point at your replica
113
+ ro_factory = build_ro_session_factory(settings)
114
+
115
+ session = ro_factory.get_session() # writes will be rejected by PostgreSQL
116
+ ```
117
+
118
+ If `DB_RO_HOST` is not set, `RODatabaseSessionFactory` falls back to the primary host but still enforces read-only mode at the PostgreSQL level.
119
+
120
+ ## FastAPI example
121
+
122
+ ```python
123
+ from contextlib import asynccontextmanager
124
+ from fastapi import FastAPI
125
+ from voltwire.db.session import DatabaseSettings, build_session_factory
126
+
127
+ settings = DatabaseSettings.from_env(".env.local")
128
+
129
+ @asynccontextmanager
130
+ async def lifespan(app: FastAPI):
131
+ app.state.db = build_session_factory(settings)
132
+ yield
133
+ app.state.db.close()
134
+
135
+ app = FastAPI(lifespan=lifespan)
136
+
137
+ @app.get("/items")
138
+ def list_items():
139
+ session = app.state.db.get_session()
140
+ try:
141
+ return session.execute(...).all()
142
+ finally:
143
+ session.close()
144
+ ```
145
+
146
+ ## Logging
147
+
148
+ The library emits to the `voltwire.db.session` logger namespace using Python's standard `logging` module. To activate debug output:
149
+
150
+ ```python
151
+ import logging
152
+ logging.getLogger("voltwire.db.session").setLevel(logging.DEBUG)
153
+ ```
154
+
155
+ ### Routing to loguru
156
+
157
+ If your app uses [loguru](https://github.com/Delgan/loguru), intercept stdlib logging once at startup:
158
+
159
+ ```python
160
+ import logging
161
+ from loguru import logger
162
+
163
+ class InterceptHandler(logging.Handler):
164
+ def emit(self, record: logging.LogRecord) -> None:
165
+ logger.opt(depth=6, exception=record.exc_info).log(
166
+ record.levelname, record.getMessage()
167
+ )
168
+
169
+ logging.getLogger("voltwire.db.session").addHandler(InterceptHandler())
170
+ ```
@@ -0,0 +1,158 @@
1
+ <img src="https://raw.githubusercontent.com/hsteidel/voltwire/main/assets/icons/db-session.svg" alt="" width="56" height="56" align="left">
2
+
3
+ # voltwire-db-session
4
+
5
+ A configurable PostgreSQL session factory for Python applications. Wraps SQLAlchemy connection pool setup and pydantic-settings configuration into a single reusable package — install it, point it at your `.env`, and get sessions.
6
+
7
+ ## ORM-agnostic core
8
+
9
+ `DatabaseSessionFactory`, `RODatabaseSessionFactory`, and `Session` (exported from `voltwire.db.session`) are `Protocol`s, not concrete classes. `TransactionContext` and `DatabaseAutoConfiguration` are written entirely against these interfaces and never import `sqlalchemy` — only `voltwire.db.session.backends.sqlalchemy` does. Today that's the only backend (`SqlAlchemyDatabaseSessionFactory`/`SqlAlchemyRODatabaseSessionFactory`, built via `build_session_factory`/`build_ro_session_factory`), but a future ORM backend only needs to satisfy the same `Protocol`s — no changes required to `TransactionContext`, `DatabaseAutoConfiguration`, or downstream packages like `voltwire-fastapi-db-txs`, which already depend only on the abstraction.
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ pip install voltwire-db-session
15
+ # or with Poetry:
16
+ poetry add voltwire-db-session
17
+ ```
18
+
19
+ A PostgreSQL driver is **not** included — install whichever you prefer alongside it:
20
+
21
+ ```bash
22
+ pip install psycopg2-binary # most common
23
+ pip install psycopg # psycopg3
24
+ ```
25
+
26
+ ## Quickstart
27
+
28
+ ```python
29
+ from voltwire.db.session import DatabaseSettings, build_session_factory
30
+
31
+ settings = DatabaseSettings() # reads DB_* vars from .env
32
+ factory = build_session_factory(settings)
33
+
34
+ session = factory.get_session()
35
+ try:
36
+ result = session.execute(...)
37
+ session.commit()
38
+ finally:
39
+ session.close()
40
+
41
+ # On app shutdown
42
+ factory.close()
43
+ ```
44
+
45
+ ## Configuration
46
+
47
+ All settings are loaded from environment variables with a `DB_` prefix. By default the library reads from a `.env` file in the working directory.
48
+
49
+ ### Environment variables
50
+
51
+ | Variable | Default | Description |
52
+ |-----------------------|---------------|--------------------------------------------------|
53
+ | `DB_HOST` | `localhost` | Primary database host |
54
+ | `DB_PORT` | `5432` | Database port |
55
+ | `DB_DATABASE` | `postgres` | Database name |
56
+ | `DB_USERNAME` | `postgres` | Database username |
57
+ | `DB_PASSWORD` | `postgres` | Database password |
58
+ | `DB_SCHEMA_NAME` | `public` | PostgreSQL schema (used for `search_path`) |
59
+ | `DB_DRIVER` | `psycopg2` | SQLAlchemy driver name |
60
+ | `DB_RO_HOST` | *(unset)* | Read-only replica host; falls back to `DB_HOST` |
61
+ | `DB_POOL_SIZE` | `10` | Minimum connections in pool |
62
+ | `DB_MAX_POOL_SIZE` | `20` | Maximum connections in pool |
63
+ | `DB_POOL_TIMEOUT` | `30` | Seconds to wait for a connection from pool |
64
+ | `DB_POOL_RECYCLE` | `299` | Recycle connections after this many seconds |
65
+ | `DB_APPLICATION_NAME` | `app` | Application name reported to PostgreSQL |
66
+
67
+ ### Choosing your env file
68
+
69
+ ```python
70
+ # Standard .env (default)
71
+ settings = DatabaseSettings()
72
+
73
+ # Custom env file — e.g. .env.local, .env.production
74
+ settings = DatabaseSettings.from_env(".env.local")
75
+
76
+ # No file — reads only from real environment variables
77
+ settings = DatabaseSettings.from_env(None)
78
+
79
+ # No file, with inline overrides
80
+ settings = DatabaseSettings.from_env(None, host="db.internal", database="myapp")
81
+ ```
82
+
83
+ ### Using a different driver
84
+
85
+ ```python
86
+ # psycopg3
87
+ settings = DatabaseSettings.from_env(".env", driver="psycopg")
88
+
89
+ # or via env var
90
+ # DB_DRIVER=psycopg
91
+ ```
92
+
93
+ The `driver` value is used as the SQLAlchemy URL scheme: `postgresql+{driver}://...`. The corresponding package must be installed in your environment.
94
+
95
+ ## Read-only replica
96
+
97
+ ```python
98
+ from voltwire.db.session import DatabaseSettings, build_ro_session_factory
99
+
100
+ settings = DatabaseSettings() # set DB_RO_HOST to point at your replica
101
+ ro_factory = build_ro_session_factory(settings)
102
+
103
+ session = ro_factory.get_session() # writes will be rejected by PostgreSQL
104
+ ```
105
+
106
+ If `DB_RO_HOST` is not set, `RODatabaseSessionFactory` falls back to the primary host but still enforces read-only mode at the PostgreSQL level.
107
+
108
+ ## FastAPI example
109
+
110
+ ```python
111
+ from contextlib import asynccontextmanager
112
+ from fastapi import FastAPI
113
+ from voltwire.db.session import DatabaseSettings, build_session_factory
114
+
115
+ settings = DatabaseSettings.from_env(".env.local")
116
+
117
+ @asynccontextmanager
118
+ async def lifespan(app: FastAPI):
119
+ app.state.db = build_session_factory(settings)
120
+ yield
121
+ app.state.db.close()
122
+
123
+ app = FastAPI(lifespan=lifespan)
124
+
125
+ @app.get("/items")
126
+ def list_items():
127
+ session = app.state.db.get_session()
128
+ try:
129
+ return session.execute(...).all()
130
+ finally:
131
+ session.close()
132
+ ```
133
+
134
+ ## Logging
135
+
136
+ The library emits to the `voltwire.db.session` logger namespace using Python's standard `logging` module. To activate debug output:
137
+
138
+ ```python
139
+ import logging
140
+ logging.getLogger("voltwire.db.session").setLevel(logging.DEBUG)
141
+ ```
142
+
143
+ ### Routing to loguru
144
+
145
+ If your app uses [loguru](https://github.com/Delgan/loguru), intercept stdlib logging once at startup:
146
+
147
+ ```python
148
+ import logging
149
+ from loguru import logger
150
+
151
+ class InterceptHandler(logging.Handler):
152
+ def emit(self, record: logging.LogRecord) -> None:
153
+ logger.opt(depth=6, exception=record.exc_info).log(
154
+ record.levelname, record.getMessage()
155
+ )
156
+
157
+ logging.getLogger("voltwire.db.session").addHandler(InterceptHandler())
158
+ ```
@@ -0,0 +1,23 @@
1
+ [project]
2
+ name = "voltwire-db-session"
3
+ version = "0.0.1"
4
+ description = "A configurable DB session factory"
5
+ authors = [{name = "Hermann Steidel", email = "hsteidel.software@gmail.com"}]
6
+ license = "MIT"
7
+ readme = "README.md"
8
+ requires-python = ">=3.13,<4.0"
9
+ dependencies = [
10
+ "sqlalchemy>=2.0,<3",
11
+ "pydantic-settings>=2.3,<3",
12
+ "pydantic>=2.9,<3",
13
+ ]
14
+
15
+ [dependency-groups]
16
+ dev = ["pytest>=8.0"]
17
+
18
+ [build-system]
19
+ requires = ["hatchling"]
20
+ build-backend = "hatchling.build"
21
+
22
+ [tool.hatch.build.targets.wheel]
23
+ packages = ["src/voltwire"]
@@ -0,0 +1,31 @@
1
+ from voltwire.db.session.autoconfiguration import (
2
+ DatabaseAutoConfiguration,
3
+ DatabaseAutoConfigurationProperties,
4
+ auto_configure_database,
5
+ )
6
+ from voltwire.db.session.backends.sqlalchemy import (
7
+ SqlAlchemyDatabaseSessionFactory,
8
+ SqlAlchemyRODatabaseSessionFactory,
9
+ build_ro_session_factory,
10
+ build_session_factory,
11
+ )
12
+ from voltwire.db.session.interfaces import DatabaseSessionFactory, RODatabaseSessionFactory, Session
13
+ from voltwire.db.session.settings import DatabaseSettings
14
+ from voltwire.db.session.transaction import TransactionContext
15
+
16
+ __version__ = "0.0.0"
17
+
18
+ __all__ = [
19
+ "DatabaseSessionFactory",
20
+ "RODatabaseSessionFactory",
21
+ "Session",
22
+ "SqlAlchemyDatabaseSessionFactory",
23
+ "SqlAlchemyRODatabaseSessionFactory",
24
+ "DatabaseSettings",
25
+ "TransactionContext",
26
+ "DatabaseAutoConfiguration",
27
+ "DatabaseAutoConfigurationProperties",
28
+ "build_session_factory",
29
+ "build_ro_session_factory",
30
+ "auto_configure_database",
31
+ ]
@@ -0,0 +1,91 @@
1
+ """
2
+ Database Autoconfiguration
3
+
4
+ Given ``DatabaseSettings`` (and optional properties for sane defaults), wires up the
5
+ components a consuming app needs for DB session + transaction management, so app
6
+ startup is a single call instead of hand-assembling each piece.
7
+
8
+ Usage::
9
+
10
+ db = auto_configure_database(get_db_settings())
11
+
12
+ # if using with di
13
+ container.register(DatabaseSessionFactory, providers.Object(db.session_factory))
14
+ container.register(TransactionContext, providers.Object(db.transaction_context))
15
+
16
+ # if using fastapi tx middleware
17
+ app.add_middleware(
18
+ TransactionMiddleware,
19
+ transaction_context=db.transaction_context,
20
+ session_factory=db.session_factory,
21
+ )
22
+ """
23
+
24
+ from collections.abc import Callable
25
+ from contextlib import contextmanager
26
+ from dataclasses import dataclass
27
+ from typing import Generator
28
+
29
+ from voltwire.db.session.backends.sqlalchemy import build_ro_session_factory, build_session_factory
30
+ from voltwire.db.session.interfaces import DatabaseSessionFactory, RODatabaseSessionFactory, Session
31
+ from voltwire.db.session.settings import DatabaseSettings
32
+ from voltwire.db.session.transaction import TransactionContext
33
+
34
+
35
+ @dataclass
36
+ class DatabaseAutoConfigurationProperties:
37
+ """Optional knobs for ``auto_configure_database``."""
38
+
39
+ build_read_replica: bool = False
40
+ on_start_transaction: Callable[[DatabaseSessionFactory], None] | None = None
41
+ """
42
+ Called with the ``DatabaseSessionFactory`` before ``transaction_context()``/
43
+ ``transaction()`` opens a session for an explicit script/background-task
44
+ transaction. Not called for sessions opened by FastAPI middleware. Use this for
45
+ validation that should only run outside the request path — e.g. asserting the
46
+ factory points at a test database during a pytest run.
47
+ """
48
+
49
+
50
+ @dataclass
51
+ class DatabaseAutoConfiguration:
52
+ """Wired DB components for a process, given ``DatabaseSettings`` + properties."""
53
+
54
+ session_factory: DatabaseSessionFactory
55
+ transaction_context: TransactionContext
56
+ ro_session_factory: RODatabaseSessionFactory | None
57
+
58
+ @contextmanager
59
+ def transaction(self) -> Generator[Session, None, None]:
60
+ """Shorthand for ``self.transaction_context.transaction(self.session_factory)``."""
61
+ with self.transaction_context.transaction(self.session_factory) as session:
62
+ yield session
63
+
64
+ @contextmanager
65
+ def read_transaction(self) -> Generator[Session, None, None]:
66
+ """
67
+ Shorthand for ``self.transaction_context.read_transaction(self.ro_session_factory)``.
68
+
69
+ Raises ``RuntimeError`` if this configuration was built without a read replica
70
+ (``DatabaseAutoConfigurationProperties.build_read_replica=False``).
71
+ """
72
+ if self.ro_session_factory is None:
73
+ raise RuntimeError(
74
+ "read_transaction() requires a read replica — "
75
+ "build with DatabaseAutoConfigurationProperties(build_read_replica=True)"
76
+ )
77
+ with self.transaction_context.read_transaction(self.ro_session_factory) as session:
78
+ yield session
79
+
80
+
81
+ def auto_configure_database(
82
+ settings: DatabaseSettings,
83
+ properties: DatabaseAutoConfigurationProperties | None = None,
84
+ ) -> DatabaseAutoConfiguration:
85
+ """Build a ``DatabaseAutoConfiguration`` from ``settings``, applying ``properties`` defaults."""
86
+ properties = properties or DatabaseAutoConfigurationProperties()
87
+ return DatabaseAutoConfiguration(
88
+ session_factory=build_session_factory(settings),
89
+ ro_session_factory=build_ro_session_factory(settings) if properties.build_read_replica else None,
90
+ transaction_context=TransactionContext(on_start_transaction=properties.on_start_transaction),
91
+ )