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.
- voltwire_db_session-0.0.1/.gitignore +221 -0
- voltwire_db_session-0.0.1/PKG-INFO +170 -0
- voltwire_db_session-0.0.1/README.md +158 -0
- voltwire_db_session-0.0.1/pyproject.toml +23 -0
- voltwire_db_session-0.0.1/src/voltwire/db/session/__init__.py +31 -0
- voltwire_db_session-0.0.1/src/voltwire/db/session/autoconfiguration.py +91 -0
- voltwire_db_session-0.0.1/src/voltwire/db/session/backends/__init__.py +0 -0
- voltwire_db_session-0.0.1/src/voltwire/db/session/backends/sqlalchemy.py +178 -0
- voltwire_db_session-0.0.1/src/voltwire/db/session/default.py +87 -0
- voltwire_db_session-0.0.1/src/voltwire/db/session/interfaces.py +45 -0
- voltwire_db_session-0.0.1/src/voltwire/db/session/py.typed +0 -0
- voltwire_db_session-0.0.1/src/voltwire/db/session/settings.py +108 -0
- voltwire_db_session-0.0.1/src/voltwire/db/session/transaction.py +221 -0
- voltwire_db_session-0.0.1/tests/test_autoconfiguration.py +111 -0
- voltwire_db_session-0.0.1/tests/test_default.py +90 -0
- voltwire_db_session-0.0.1/tests/test_interfaces.py +124 -0
- voltwire_db_session-0.0.1/tests/test_sample.py +6 -0
- voltwire_db_session-0.0.1/tests/test_session_factory.py +182 -0
- voltwire_db_session-0.0.1/tests/test_transaction.py +211 -0
|
@@ -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
|
+
)
|
|
File without changes
|