postgres-component 0.1.0__py3-none-any.whl

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,10 @@
1
+ from __future__ import annotations
2
+
3
+ from postgres_component.component import PostgresComponent
4
+ from postgres_component.migrations import MigrationWiring, create_migration_engine
5
+
6
+ __all__ = [
7
+ "MigrationWiring",
8
+ "PostgresComponent",
9
+ "create_migration_engine",
10
+ ]
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable
4
+
5
+ from fastapi import APIRouter
6
+
7
+
8
+ def build_health_router(get_status: Callable[[], dict[str, Any]]) -> APIRouter:
9
+ router = APIRouter()
10
+
11
+ @router.get("/health")
12
+ async def health() -> dict[str, Any]:
13
+ return get_status()
14
+
15
+ return router
@@ -0,0 +1,109 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import AsyncIterator
4
+ from contextlib import asynccontextmanager
5
+ from typing import Any
6
+
7
+ from fastapi import APIRouter
8
+ from python_components import Component
9
+ from sqlalchemy import URL, make_url, text
10
+ from sqlalchemy.ext.asyncio import (
11
+ AsyncEngine,
12
+ AsyncSession,
13
+ async_sessionmaker,
14
+ create_async_engine,
15
+ )
16
+
17
+ from postgres_component._routes import build_health_router
18
+ from postgres_component.migrations import MigrationWiring
19
+
20
+
21
+ class PostgresComponent(Component):
22
+ def __init__(
23
+ self,
24
+ *,
25
+ url: str | None = None,
26
+ host: str | None = None,
27
+ port: int | None = None,
28
+ user: str | None = None,
29
+ password: str | None = None,
30
+ database: str | None = None,
31
+ **engine_kwargs: Any,
32
+ ) -> None:
33
+ self.using([])
34
+ self._url = url or self._build_url(host, port, user, password, database)
35
+ self._engine_kwargs = engine_kwargs
36
+ self._engine: AsyncEngine | None = None
37
+ self._connected = False
38
+ self._last_error: str | None = None
39
+ self.session_factory: async_sessionmaker[AsyncSession] | None = None
40
+
41
+ @staticmethod
42
+ def _build_url(
43
+ host: str | None,
44
+ port: int | None,
45
+ user: str | None,
46
+ password: str | None,
47
+ database: str | None,
48
+ ) -> str:
49
+ if host is None or user is None or database is None:
50
+ raise TypeError(
51
+ "PostgresComponent requires either `url` or `host`/`user`/`database`"
52
+ )
53
+ return URL.create(
54
+ "postgresql+asyncpg",
55
+ username=user,
56
+ password=password,
57
+ host=host,
58
+ port=port or 5432,
59
+ database=database,
60
+ ).render_as_string(hide_password=False)
61
+
62
+ def _sanitize_error(self, message: str) -> str:
63
+ password = make_url(self._url).password
64
+ if password:
65
+ return message.replace(password, "***")
66
+ return message
67
+
68
+ async def start(self) -> None:
69
+ if self._engine is not None:
70
+ await self._engine.dispose()
71
+ engine = create_async_engine(self._url, **self._engine_kwargs)
72
+ try:
73
+ async with engine.connect() as conn:
74
+ await conn.execute(text("SELECT 1"))
75
+ except Exception as exc:
76
+ await engine.dispose()
77
+ self._last_error = self._sanitize_error(type(exc).__name__)
78
+ raise
79
+ self._engine = engine
80
+ self.session_factory = async_sessionmaker(engine, expire_on_commit=False)
81
+ self._connected = True
82
+ self._last_error = None
83
+
84
+ async def shutdown(self) -> None:
85
+ if self._engine is not None:
86
+ await self._engine.dispose()
87
+ self._connected = False
88
+ self.session_factory = None
89
+
90
+ @asynccontextmanager
91
+ async def session(self) -> AsyncIterator[AsyncSession]:
92
+ if self.session_factory is None:
93
+ raise RuntimeError("PostgresComponent.session() called before start()")
94
+ async with self.session_factory() as session:
95
+ try:
96
+ async with session.begin():
97
+ yield session
98
+ except Exception as exc:
99
+ self._last_error = self._sanitize_error(type(exc).__name__)
100
+ raise
101
+
102
+ def routes(self) -> APIRouter:
103
+ return build_health_router(self.get_status)
104
+
105
+ def get_status(self) -> dict[str, Any]:
106
+ return {"connected": self._connected, "last_error": self._last_error}
107
+
108
+ def migration_wiring(self) -> MigrationWiring:
109
+ return MigrationWiring(url=self._url, engine_kwargs=dict(self._engine_kwargs))
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class MigrationWiring:
11
+ url: str
12
+ engine_kwargs: dict[str, Any]
13
+
14
+
15
+ def create_migration_engine(wiring: MigrationWiring) -> AsyncEngine:
16
+ return create_async_engine(wiring.url, **wiring.engine_kwargs)
File without changes
@@ -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,9 @@
1
+ postgres_component/__init__.py,sha256=rxVd737FJ8JArAJF2f_sz1yy9fOuESwx4PBrMGcbr84,272
2
+ postgres_component/_routes.py,sha256=Hw607vvqEICruIcQ-5wcNWMwKk16wdrOjdbh84loeZs,324
3
+ postgres_component/component.py,sha256=dPFaFf8wARCr8zPzUt0qVdP4GVuSlXOKl8DFPpPzPXs,3672
4
+ postgres_component/migrations.py,sha256=G149lyepn2ZLo7mVUzCaEyjTdZc7spFQrB7BM5rx3-s,396
5
+ postgres_component/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ postgres_component-0.1.0.dist-info/METADATA,sha256=NJY4p4Gbn9UUFodkHxPOGp2WOqktqyeQeDhBXhhJOT0,3905
7
+ postgres_component-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
8
+ postgres_component-0.1.0.dist-info/licenses/LICENSE,sha256=B-SYSQHSY79FyOS46ZHL43ywEGFd_VyMLDiBw-9cQYk,1070
9
+ postgres_component-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.