postgres-component 0.1.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.
- postgres_component-0.1.0/.github/workflows/ci.yml +23 -0
- postgres_component-0.1.0/.github/workflows/publish-pypi.yml +31 -0
- postgres_component-0.1.0/.gitignore +6 -0
- postgres_component-0.1.0/.python-version +1 -0
- postgres_component-0.1.0/CHANGELOG.md +5 -0
- postgres_component-0.1.0/CONTEXT.md +35 -0
- postgres_component-0.1.0/LICENSE +21 -0
- postgres_component-0.1.0/PKG-INFO +111 -0
- postgres_component-0.1.0/README.md +86 -0
- postgres_component-0.1.0/docker-compose.yml +9 -0
- postgres_component-0.1.0/docs/adr/0001-sqlalchemy-async-with-asyncpg.md +34 -0
- postgres_component-0.1.0/docs/adr/0002-migrations-stay-app-owned.md +31 -0
- postgres_component-0.1.0/docs/superpowers/plans/2026-09-30-postgres-component.md +991 -0
- postgres_component-0.1.0/postgres_component/__init__.py +10 -0
- postgres_component-0.1.0/postgres_component/_routes.py +15 -0
- postgres_component-0.1.0/postgres_component/component.py +109 -0
- postgres_component-0.1.0/postgres_component/migrations.py +16 -0
- postgres_component-0.1.0/postgres_component/py.typed +0 -0
- postgres_component-0.1.0/postgres_component/tests/__init__.py +0 -0
- postgres_component-0.1.0/postgres_component/tests/conftest.py +15 -0
- postgres_component-0.1.0/postgres_component/tests/test_component.py +262 -0
- postgres_component-0.1.0/postgres_component/tests/test_migrations.py +16 -0
- postgres_component-0.1.0/postgres_component/tests/test_routes.py +21 -0
- postgres_component-0.1.0/postgres_component/tests/test_system_integration.py +31 -0
- postgres_component-0.1.0/pyproject.toml +46 -0
- postgres_component-0.1.0/pytest.ini +7 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_call:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: astral-sh/setup-uv@v3
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
- run: uv sync --all-groups
|
|
21
|
+
- run: uv run pytest
|
|
22
|
+
- run: uv run ruff format --check .
|
|
23
|
+
- run: uv run ruff check .
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
inputs:
|
|
8
|
+
target:
|
|
9
|
+
description: "PyPI target"
|
|
10
|
+
required: true
|
|
11
|
+
default: "pypi"
|
|
12
|
+
type: choice
|
|
13
|
+
options: ["pypi", "testpypi"]
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
ci:
|
|
17
|
+
uses: ./.github/workflows/ci.yml
|
|
18
|
+
|
|
19
|
+
publish:
|
|
20
|
+
needs: ci
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
environment: ${{ github.event.inputs.target || 'pypi' }}
|
|
23
|
+
permissions:
|
|
24
|
+
id-token: write
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v4
|
|
27
|
+
- uses: astral-sh/setup-uv@v3
|
|
28
|
+
- run: uv build
|
|
29
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
30
|
+
with:
|
|
31
|
+
repository-url: ${{ (github.event.inputs.target == 'testpypi') && 'https://test.pypi.org/legacy/' || '' }}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# postgres-component
|
|
2
|
+
|
|
3
|
+
Component library for Postgres connection lifecycle, built on python-components'
|
|
4
|
+
`Component` contract. Wraps a SQLAlchemy async engine so it participates in a
|
|
5
|
+
`System`'s dependency graph, without owning schema or query concerns.
|
|
6
|
+
|
|
7
|
+
## Language
|
|
8
|
+
|
|
9
|
+
**PostgresComponent**:
|
|
10
|
+
A `Component` that manages a SQLAlchemy async engine's lifecycle — verifies
|
|
11
|
+
connectivity on `start()`, disposes the engine on `shutdown()` — and hands out
|
|
12
|
+
session access to consumers. Owns connection lifecycle only: no models, no
|
|
13
|
+
migrations, no query API of its own.
|
|
14
|
+
_Avoid_: Database component, Postgres client, DB wrapper
|
|
15
|
+
|
|
16
|
+
**SessionFactory**:
|
|
17
|
+
The raw `async_sessionmaker` exposed by `PostgresComponent` (`session_factory`
|
|
18
|
+
attribute) for callers who need manual control over a session's transaction
|
|
19
|
+
boundaries — long-lived reads, streaming, anything that doesn't fit one
|
|
20
|
+
begin/commit cycle.
|
|
21
|
+
_Avoid_: raw session, sessionmaker, engine session
|
|
22
|
+
|
|
23
|
+
**Managed Session**:
|
|
24
|
+
The `session()` async context manager `PostgresComponent` exposes: begins a
|
|
25
|
+
transaction, commits on clean exit, rolls back on exception. The default way
|
|
26
|
+
consumers should talk to the database — one call, one unit of work. Distinct
|
|
27
|
+
from a `SessionFactory` session, which commits nothing on its own.
|
|
28
|
+
_Avoid_: session(), auto-commit session, transaction wrapper, unit of work
|
|
29
|
+
|
|
30
|
+
**Migration Wiring**:
|
|
31
|
+
The optional helper `PostgresComponent` exposes so a consuming app's own
|
|
32
|
+
Alembic `env.py` can reuse the same async engine and driver already
|
|
33
|
+
configured on the component. `PostgresComponent` never runs migrations
|
|
34
|
+
itself — this only removes the boilerplate of re-deriving connection info.
|
|
35
|
+
_Avoid_: auto-migrate, migration runner, schema sync
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fabricio Lima
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: postgres-component
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Async Postgres connection-lifecycle component for python-components
|
|
5
|
+
Project-URL: Homepage, https://github.com/fabriciooml/postgres-component
|
|
6
|
+
Project-URL: Repository, https://github.com/fabriciooml/postgres-component
|
|
7
|
+
Project-URL: Issues, https://github.com/fabriciooml/postgres-component/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/fabriciooml/postgres-component/blob/main/CHANGELOG.md
|
|
9
|
+
Author: Fabricio Lima
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: asyncio,asyncpg,postgres,postgresql,python-components,sqlalchemy
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: asyncpg<1,>=0.29
|
|
21
|
+
Requires-Dist: fastapi>=0.115
|
|
22
|
+
Requires-Dist: python-components<0.5,>=0.4.0
|
|
23
|
+
Requires-Dist: sqlalchemy[asyncio]<3,>=2.0
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# postgres-component
|
|
27
|
+
|
|
28
|
+
Async Postgres connection-lifecycle component for [python-components](https://github.com/lucassant95/python-components).
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
uv add postgres-component
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Requirements
|
|
37
|
+
|
|
38
|
+
- Python >= 3.11
|
|
39
|
+
- A running Postgres server
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
import asyncio
|
|
45
|
+
|
|
46
|
+
from python_components import System
|
|
47
|
+
from postgres_component import PostgresComponent
|
|
48
|
+
|
|
49
|
+
database = PostgresComponent(url="postgresql+asyncpg://user:pass@localhost:5432/mydb")
|
|
50
|
+
|
|
51
|
+
system = System({"database": database})
|
|
52
|
+
|
|
53
|
+
async def main():
|
|
54
|
+
async with system:
|
|
55
|
+
async with database.session() as session:
|
|
56
|
+
await session.execute(...) # commits on success, rolls back on exception
|
|
57
|
+
|
|
58
|
+
asyncio.run(main())
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`PostgresComponent` can also be built from individual kwargs instead of a URL:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
PostgresComponent(host="localhost", port=5432, user="user", password="pass", database="mydb")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`url` takes priority over the kwargs when both are given.
|
|
68
|
+
|
|
69
|
+
### Health route
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from fastapi_component import create_app
|
|
73
|
+
|
|
74
|
+
app = create_app(system) # RouteProvider discovery adds GET /health automatically
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Migrations
|
|
78
|
+
|
|
79
|
+
`PostgresComponent` never runs migrations itself. `migration_wiring()` hands your
|
|
80
|
+
own Alembic `env.py` the same connection config so it doesn't have to be
|
|
81
|
+
re-derived:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
# alembic/env.py
|
|
85
|
+
from postgres_component import create_migration_engine
|
|
86
|
+
|
|
87
|
+
engine = create_migration_engine(database.migration_wiring())
|
|
88
|
+
# use `engine` with Alembic's async run_sync() pattern
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Semantics and caveats
|
|
92
|
+
|
|
93
|
+
- **Fail-fast startup.** `start()` runs an explicit `SELECT 1` before considering itself connected — a dead database makes `start()` raise rather than "starting" successfully and failing later on first query.
|
|
94
|
+
- **Managed Session is a unit of work.** `database.session()` begins a transaction, commits on clean exit, and rolls back on exception — one call is one unit of work. For manual control (long-lived reads, streaming), use the raw `database.session_factory` directly.
|
|
95
|
+
- **Errors propagate raw.** SQLAlchemy exceptions (`OperationalError`, `IntegrityError`, etc.) are never wrapped; `get_status()["last_error"]` is updated as a string for introspection, but the original exception type always reaches the caller.
|
|
96
|
+
- **No ORM, no migrations.** The component owns connection lifecycle only. Models and Alembic setup belong to the consuming app; `migration_wiring()` only removes the boilerplate of re-deriving connection config.
|
|
97
|
+
|
|
98
|
+
## Development
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
uv sync --all-groups
|
|
102
|
+
uv run pytest
|
|
103
|
+
uv run ruff format --check .
|
|
104
|
+
uv run ruff check .
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Integration tests spin up a real Postgres instance via [testcontainers](https://testcontainers.com/modules/postgres/). For local development against a persistent instance instead, run `docker compose up -d`.
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
MIT
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# postgres-component
|
|
2
|
+
|
|
3
|
+
Async Postgres connection-lifecycle component for [python-components](https://github.com/lucassant95/python-components).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uv add postgres-component
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
- Python >= 3.11
|
|
14
|
+
- A running Postgres server
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
import asyncio
|
|
20
|
+
|
|
21
|
+
from python_components import System
|
|
22
|
+
from postgres_component import PostgresComponent
|
|
23
|
+
|
|
24
|
+
database = PostgresComponent(url="postgresql+asyncpg://user:pass@localhost:5432/mydb")
|
|
25
|
+
|
|
26
|
+
system = System({"database": database})
|
|
27
|
+
|
|
28
|
+
async def main():
|
|
29
|
+
async with system:
|
|
30
|
+
async with database.session() as session:
|
|
31
|
+
await session.execute(...) # commits on success, rolls back on exception
|
|
32
|
+
|
|
33
|
+
asyncio.run(main())
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`PostgresComponent` can also be built from individual kwargs instead of a URL:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
PostgresComponent(host="localhost", port=5432, user="user", password="pass", database="mydb")
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`url` takes priority over the kwargs when both are given.
|
|
43
|
+
|
|
44
|
+
### Health route
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from fastapi_component import create_app
|
|
48
|
+
|
|
49
|
+
app = create_app(system) # RouteProvider discovery adds GET /health automatically
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Migrations
|
|
53
|
+
|
|
54
|
+
`PostgresComponent` never runs migrations itself. `migration_wiring()` hands your
|
|
55
|
+
own Alembic `env.py` the same connection config so it doesn't have to be
|
|
56
|
+
re-derived:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
# alembic/env.py
|
|
60
|
+
from postgres_component import create_migration_engine
|
|
61
|
+
|
|
62
|
+
engine = create_migration_engine(database.migration_wiring())
|
|
63
|
+
# use `engine` with Alembic's async run_sync() pattern
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Semantics and caveats
|
|
67
|
+
|
|
68
|
+
- **Fail-fast startup.** `start()` runs an explicit `SELECT 1` before considering itself connected — a dead database makes `start()` raise rather than "starting" successfully and failing later on first query.
|
|
69
|
+
- **Managed Session is a unit of work.** `database.session()` begins a transaction, commits on clean exit, and rolls back on exception — one call is one unit of work. For manual control (long-lived reads, streaming), use the raw `database.session_factory` directly.
|
|
70
|
+
- **Errors propagate raw.** SQLAlchemy exceptions (`OperationalError`, `IntegrityError`, etc.) are never wrapped; `get_status()["last_error"]` is updated as a string for introspection, but the original exception type always reaches the caller.
|
|
71
|
+
- **No ORM, no migrations.** The component owns connection lifecycle only. Models and Alembic setup belong to the consuming app; `migration_wiring()` only removes the boilerplate of re-deriving connection config.
|
|
72
|
+
|
|
73
|
+
## Development
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
uv sync --all-groups
|
|
77
|
+
uv run pytest
|
|
78
|
+
uv run ruff format --check .
|
|
79
|
+
uv run ruff check .
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Integration tests spin up a real Postgres instance via [testcontainers](https://testcontainers.com/modules/postgres/). For local development against a persistent instance instead, run `docker compose up -d`.
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
status: accepted
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# SQLAlchemy async engine with asyncpg, eager connectivity check on start()
|
|
6
|
+
|
|
7
|
+
postgres-component needs to pick a layer for talking to Postgres. We chose
|
|
8
|
+
**SQLAlchemy's async engine** over a raw driver (asyncpg directly, matching
|
|
9
|
+
kafka-component's raw-`aiokafka` precedent) because it buys ORM mapping and
|
|
10
|
+
Alembic migration tooling for consumers, and the one real Postgres user in
|
|
11
|
+
this codebase (`k9`) already models its data with SQLAlchemy's ORM. Under
|
|
12
|
+
SQLAlchemy we picked **asyncpg** over psycopg3-async as the DBAPI: it's the
|
|
13
|
+
more common pairing in SQLAlchemy's own async docs and examples.
|
|
14
|
+
|
|
15
|
+
SQLAlchemy engines are lazy by default — no connection opens until first use.
|
|
16
|
+
That breaks the family's documented fail-fast startup contract (a failing
|
|
17
|
+
`start()` rolls back already-started components and aborts the server before
|
|
18
|
+
it serves traffic), so `PostgresComponent.start()` runs an explicit
|
|
19
|
+
connectivity check (`SELECT 1`) rather than trusting the engine's laziness.
|
|
20
|
+
Query-time exceptions (`OperationalError`, `IntegrityError`, etc.) propagate
|
|
21
|
+
raw — SQLAlchemy's own exception hierarchy is already the right vocabulary,
|
|
22
|
+
and there's no behavioral policy here (unlike kafka's `ErrorPolicy`) to
|
|
23
|
+
justify a wrapping layer.
|
|
24
|
+
|
|
25
|
+
## Considered Options
|
|
26
|
+
|
|
27
|
+
- Raw asyncpg, no SQLAlchemy (rejected: no ORM/migration story, and k9's
|
|
28
|
+
existing Postgres usage is already ORM-shaped)
|
|
29
|
+
- psycopg3-async as the DBAPI (rejected: asyncpg is the more common
|
|
30
|
+
SQLAlchemy-async pairing, no concrete reason here to deviate)
|
|
31
|
+
- Lazy engine, no startup connectivity check (rejected: silently breaks the
|
|
32
|
+
family's fail-fast startup guarantee — a dead DB would "start" successfully)
|
|
33
|
+
- Wrapping SQLAlchemy exceptions in component-specific error types (rejected:
|
|
34
|
+
no behavioral difference to encode, just translation boilerplate)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
status: accepted
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Migrations stay app-owned; component ships wiring only
|
|
6
|
+
|
|
7
|
+
`PostgresComponent` does not run, own, or auto-trigger schema migrations.
|
|
8
|
+
Consuming apps run their own Alembic setup against the same connection info.
|
|
9
|
+
The component's only concession is an optional **Migration Wiring** helper
|
|
10
|
+
that lets an app's `env.py` reuse the same async engine/driver instead of
|
|
11
|
+
re-deriving it — targeting Alembic's async template (`run_sync`) so no second,
|
|
12
|
+
sync-only driver dependency (e.g. `psycopg2`) is needed alongside `asyncpg`.
|
|
13
|
+
|
|
14
|
+
The alternative — auto-running migrations inside `start()` — was rejected
|
|
15
|
+
mainly because it's unsafe under concurrent startup: multiple replicas
|
|
16
|
+
booting together would race on the same migration unless the component also
|
|
17
|
+
implemented a distributed lock (e.g. a Postgres advisory lock), which is real
|
|
18
|
+
scope the component doesn't need to own. It would also be the first component
|
|
19
|
+
in the family to mix deploy-time schema DDL into a runtime lifecycle hook —
|
|
20
|
+
none of kafka-component (doesn't create topics) or prometheus-component
|
|
21
|
+
(doesn't push metrics) do anything comparable.
|
|
22
|
+
|
|
23
|
+
## Considered Options
|
|
24
|
+
|
|
25
|
+
- Auto-run migrations in `start()`, opt-in flag (rejected: races under
|
|
26
|
+
multi-replica startup without a distributed lock the component would have
|
|
27
|
+
to implement; no precedent elsewhere in the family)
|
|
28
|
+
- Fully hands-off, no helper at all (rejected: leaves every consumer
|
|
29
|
+
re-deriving the same connection wiring in its own `env.py` for no benefit)
|
|
30
|
+
- Component ships its own lightweight SQL-file migrator instead of Alembic
|
|
31
|
+
(rejected: reinvents a weaker version of an existing, well-known tool)
|