openframe-adapters-db-postgres 1.0.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.
- openframe_adapters_db_postgres-1.0.0/.gitignore +218 -0
- openframe_adapters_db_postgres-1.0.0/PKG-INFO +156 -0
- openframe_adapters_db_postgres-1.0.0/README.md +141 -0
- openframe_adapters_db_postgres-1.0.0/openframe/adapters/db/postgres/__init__.py +47 -0
- openframe_adapters_db_postgres-1.0.0/openframe/adapters/db/postgres/config.py +59 -0
- openframe_adapters_db_postgres-1.0.0/openframe/adapters/db/postgres/connection.py +99 -0
- openframe_adapters_db_postgres-1.0.0/openframe/adapters/db/postgres/repository.py +419 -0
- openframe_adapters_db_postgres-1.0.0/pyproject.toml +30 -0
- openframe_adapters_db_postgres-1.0.0/tests/conftest.py +58 -0
- openframe_adapters_db_postgres-1.0.0/tests/test_config.py +62 -0
- openframe_adapters_db_postgres-1.0.0/tests/test_connection.py +124 -0
- openframe_adapters_db_postgres-1.0.0/tests/test_health.py +84 -0
- openframe_adapters_db_postgres-1.0.0/tests/test_repository.py +216 -0
|
@@ -0,0 +1,218 @@
|
|
|
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
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openframe-adapters-db-postgres
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: OpenFrame Microservice Suite — PostgreSQL database adapter.
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: asyncpg,hexagonal,microservice,openframe,postgres
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Requires-Dist: asyncpg>=0.29
|
|
9
|
+
Requires-Dist: openframe-core<2,>=1.0
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
12
|
+
Requires-Dist: pytest-mock>=3.14; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# openframe-adapters-db-postgres
|
|
17
|
+
|
|
18
|
+
PostgreSQL database adapter for the **OpenFrame Microservice Suite**.
|
|
19
|
+
|
|
20
|
+
Part of the `openframe-adapters` monorepo. Implements `BaseRepository[T]` and
|
|
21
|
+
`HealthCheck` from `openframe-core` using `asyncpg`.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install openframe-adapters-db-postgres
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Required env var:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
DATABASE_URL=postgresql://user:password@host:5432/dbname
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Quick start
|
|
40
|
+
|
|
41
|
+
### Raw dict mode
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from openframe.adapters.db.postgres import PostgresSettings, PostgresRepository
|
|
45
|
+
|
|
46
|
+
settings = PostgresSettings() # reads DATABASE_URL from env
|
|
47
|
+
repo = PostgresRepository(settings, table="items", id_column="id")
|
|
48
|
+
|
|
49
|
+
item = await repo.get("abc-123") # dict | None
|
|
50
|
+
items, total = await repo.list(10, 0) # ([dict, ...], int)
|
|
51
|
+
created = await repo.create({"name": "x"})
|
|
52
|
+
updated = await repo.update({"id": "abc-123", "name": "y"})
|
|
53
|
+
deleted = await repo.delete("abc-123") # bool
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Typed domain mode
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from dataclasses import dataclass
|
|
60
|
+
from openframe.adapters.db.postgres import PostgresSettings, PostgresRepository
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class Item:
|
|
64
|
+
id: str
|
|
65
|
+
name: str
|
|
66
|
+
|
|
67
|
+
class ItemRepository(PostgresRepository[Item]):
|
|
68
|
+
_table = "items"
|
|
69
|
+
_id_column = "id"
|
|
70
|
+
|
|
71
|
+
def _row_to_entity(self, row) -> Item:
|
|
72
|
+
return Item(**dict(row))
|
|
73
|
+
|
|
74
|
+
def _entity_to_row(self, entity: Item) -> dict:
|
|
75
|
+
return {"id": entity.id, "name": entity.name}
|
|
76
|
+
|
|
77
|
+
settings = PostgresSettings()
|
|
78
|
+
repo = ItemRepository(settings)
|
|
79
|
+
item: Item | None = await repo.get("abc-123")
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Configuration
|
|
85
|
+
|
|
86
|
+
All settings are read from environment variables.
|
|
87
|
+
|
|
88
|
+
| Env var | Type | Default | Description |
|
|
89
|
+
|---|---|---|---|
|
|
90
|
+
| `DATABASE_URL` | `str` | **required** | Full asyncpg DSN |
|
|
91
|
+
| `POOL_SIZE` | `int` | `10` | Pool min/max size |
|
|
92
|
+
| `POOL_MAX_INACTIVE_CONN_LIFETIME` | `float` | `300.0` | Idle connection TTL (s) |
|
|
93
|
+
| `POOL_COMMAND_TIMEOUT` | `float` | `60.0` | Per-statement timeout (s) |
|
|
94
|
+
| `POOL_MAX_QUERIES` | `int` | `50000` | Queries per connection before recycle |
|
|
95
|
+
| `CONNECTION_TIMEOUT` | `float` | `30.0` | Pool creation timeout (s) |
|
|
96
|
+
| `OPERATION_TIMEOUT` | `float` | `10.0` | Per-operation timeout (s) |
|
|
97
|
+
| `MAX_RETRIES` | `int` | `3` | Max retry attempts |
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Health checks
|
|
102
|
+
|
|
103
|
+
`PostgresRepository` implements the `HealthCheck` protocol from `openframe-core`.
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
alive = await repo.ping() # SELECT 1 — fast liveness check
|
|
107
|
+
ready = await repo.is_ready() # pg_tables query — full readiness check
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Both methods return `False` on any failure and never raise.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Exception hierarchy
|
|
115
|
+
|
|
116
|
+
All exceptions are `AdapterError` subclasses from `openframe.core.exceptions`.
|
|
117
|
+
Raw `asyncpg` exceptions never escape the adapter.
|
|
118
|
+
|
|
119
|
+
| Situation | Exception |
|
|
120
|
+
|---|---|
|
|
121
|
+
| Cannot connect to Postgres | `AdapterConnectionError` |
|
|
122
|
+
| Invalid `DATABASE_URL` catalog | `AdapterConfigurationError` |
|
|
123
|
+
| Query failed (constraint, syntax, etc.) | `AdapterQueryError` |
|
|
124
|
+
| Entity not found | `AdapterNotFoundError` |
|
|
125
|
+
| Operation exceeded timeout | `AdapterTimeoutError` |
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Development
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# from the package directory
|
|
133
|
+
pip install -e ".[dev]"
|
|
134
|
+
pytest tests/ -v
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Protocol conformance
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from openframe.core.ports import BaseRepository
|
|
143
|
+
from openframe.core.health import HealthCheck
|
|
144
|
+
|
|
145
|
+
repo = PostgresRepository(settings, table="items", id_column="id")
|
|
146
|
+
assert isinstance(repo, BaseRepository) # True — structural check
|
|
147
|
+
assert isinstance(repo, HealthCheck) # True — structural check
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
No inheritance from either Protocol is required or used.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# openframe-adapters-db-postgres
|
|
2
|
+
|
|
3
|
+
PostgreSQL database adapter for the **OpenFrame Microservice Suite**.
|
|
4
|
+
|
|
5
|
+
Part of the `openframe-adapters` monorepo. Implements `BaseRepository[T]` and
|
|
6
|
+
`HealthCheck` from `openframe-core` using `asyncpg`.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install openframe-adapters-db-postgres
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Required env var:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
DATABASE_URL=postgresql://user:password@host:5432/dbname
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
### Raw dict mode
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from openframe.adapters.db.postgres import PostgresSettings, PostgresRepository
|
|
30
|
+
|
|
31
|
+
settings = PostgresSettings() # reads DATABASE_URL from env
|
|
32
|
+
repo = PostgresRepository(settings, table="items", id_column="id")
|
|
33
|
+
|
|
34
|
+
item = await repo.get("abc-123") # dict | None
|
|
35
|
+
items, total = await repo.list(10, 0) # ([dict, ...], int)
|
|
36
|
+
created = await repo.create({"name": "x"})
|
|
37
|
+
updated = await repo.update({"id": "abc-123", "name": "y"})
|
|
38
|
+
deleted = await repo.delete("abc-123") # bool
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Typed domain mode
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from dataclasses import dataclass
|
|
45
|
+
from openframe.adapters.db.postgres import PostgresSettings, PostgresRepository
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class Item:
|
|
49
|
+
id: str
|
|
50
|
+
name: str
|
|
51
|
+
|
|
52
|
+
class ItemRepository(PostgresRepository[Item]):
|
|
53
|
+
_table = "items"
|
|
54
|
+
_id_column = "id"
|
|
55
|
+
|
|
56
|
+
def _row_to_entity(self, row) -> Item:
|
|
57
|
+
return Item(**dict(row))
|
|
58
|
+
|
|
59
|
+
def _entity_to_row(self, entity: Item) -> dict:
|
|
60
|
+
return {"id": entity.id, "name": entity.name}
|
|
61
|
+
|
|
62
|
+
settings = PostgresSettings()
|
|
63
|
+
repo = ItemRepository(settings)
|
|
64
|
+
item: Item | None = await repo.get("abc-123")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Configuration
|
|
70
|
+
|
|
71
|
+
All settings are read from environment variables.
|
|
72
|
+
|
|
73
|
+
| Env var | Type | Default | Description |
|
|
74
|
+
|---|---|---|---|
|
|
75
|
+
| `DATABASE_URL` | `str` | **required** | Full asyncpg DSN |
|
|
76
|
+
| `POOL_SIZE` | `int` | `10` | Pool min/max size |
|
|
77
|
+
| `POOL_MAX_INACTIVE_CONN_LIFETIME` | `float` | `300.0` | Idle connection TTL (s) |
|
|
78
|
+
| `POOL_COMMAND_TIMEOUT` | `float` | `60.0` | Per-statement timeout (s) |
|
|
79
|
+
| `POOL_MAX_QUERIES` | `int` | `50000` | Queries per connection before recycle |
|
|
80
|
+
| `CONNECTION_TIMEOUT` | `float` | `30.0` | Pool creation timeout (s) |
|
|
81
|
+
| `OPERATION_TIMEOUT` | `float` | `10.0` | Per-operation timeout (s) |
|
|
82
|
+
| `MAX_RETRIES` | `int` | `3` | Max retry attempts |
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Health checks
|
|
87
|
+
|
|
88
|
+
`PostgresRepository` implements the `HealthCheck` protocol from `openframe-core`.
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
alive = await repo.ping() # SELECT 1 — fast liveness check
|
|
92
|
+
ready = await repo.is_ready() # pg_tables query — full readiness check
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Both methods return `False` on any failure and never raise.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Exception hierarchy
|
|
100
|
+
|
|
101
|
+
All exceptions are `AdapterError` subclasses from `openframe.core.exceptions`.
|
|
102
|
+
Raw `asyncpg` exceptions never escape the adapter.
|
|
103
|
+
|
|
104
|
+
| Situation | Exception |
|
|
105
|
+
|---|---|
|
|
106
|
+
| Cannot connect to Postgres | `AdapterConnectionError` |
|
|
107
|
+
| Invalid `DATABASE_URL` catalog | `AdapterConfigurationError` |
|
|
108
|
+
| Query failed (constraint, syntax, etc.) | `AdapterQueryError` |
|
|
109
|
+
| Entity not found | `AdapterNotFoundError` |
|
|
110
|
+
| Operation exceeded timeout | `AdapterTimeoutError` |
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Development
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# from the package directory
|
|
118
|
+
pip install -e ".[dev]"
|
|
119
|
+
pytest tests/ -v
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Protocol conformance
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from openframe.core.ports import BaseRepository
|
|
128
|
+
from openframe.core.health import HealthCheck
|
|
129
|
+
|
|
130
|
+
repo = PostgresRepository(settings, table="items", id_column="id")
|
|
131
|
+
assert isinstance(repo, BaseRepository) # True — structural check
|
|
132
|
+
assert isinstance(repo, HealthCheck) # True — structural check
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
No inheritance from either Protocol is required or used.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
MIT
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
openframe.adapters.db.postgres
|
|
3
|
+
================================
|
|
4
|
+
PostgreSQL database adapter for the OpenFrame Microservice Suite.
|
|
5
|
+
|
|
6
|
+
Public API:
|
|
7
|
+
|
|
8
|
+
PostgresSettings — Pydantic Settings subclass for connection config.
|
|
9
|
+
PostgresRepository — Generic async repository (BaseRepository + HealthCheck).
|
|
10
|
+
get_postgres_pool — Async factory that creates / returns the cached pool.
|
|
11
|
+
|
|
12
|
+
Quick start::
|
|
13
|
+
|
|
14
|
+
from openframe.adapters.db.postgres import (
|
|
15
|
+
PostgresSettings,
|
|
16
|
+
PostgresRepository,
|
|
17
|
+
get_postgres_pool,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
settings = PostgresSettings(database_url="postgresql://user:pw@localhost/db")
|
|
21
|
+
|
|
22
|
+
# Raw dict mode
|
|
23
|
+
repo = PostgresRepository(settings, table="items", id_column="id")
|
|
24
|
+
item = await repo.get("abc-123") # dict | None
|
|
25
|
+
|
|
26
|
+
# Typed mode — subclass and override mapping methods
|
|
27
|
+
class ItemRepository(PostgresRepository[Item]):
|
|
28
|
+
_table = "items"
|
|
29
|
+
_id_column = "id"
|
|
30
|
+
|
|
31
|
+
def _row_to_entity(self, row):
|
|
32
|
+
return Item(**dict(row))
|
|
33
|
+
|
|
34
|
+
def _entity_to_row(self, entity):
|
|
35
|
+
return entity.model_dump()
|
|
36
|
+
"""
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
from .config import PostgresSettings
|
|
40
|
+
from .connection import get_postgres_pool
|
|
41
|
+
from .repository import PostgresRepository
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"PostgresSettings",
|
|
45
|
+
"PostgresRepository",
|
|
46
|
+
"get_postgres_pool",
|
|
47
|
+
]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
openframe/adapters/db/postgres/config.py
|
|
3
|
+
=========================================
|
|
4
|
+
PostgreSQL adapter settings.
|
|
5
|
+
|
|
6
|
+
Reads all connection configuration from environment variables via Pydantic
|
|
7
|
+
Settings. Every field is validated at instantiation — missing required fields
|
|
8
|
+
raise ``pydantic_core.ValidationError`` immediately so misconfigured
|
|
9
|
+
deployments fail fast on startup.
|
|
10
|
+
|
|
11
|
+
Required env vars:
|
|
12
|
+
DATABASE_URL: Full asyncpg DSN.
|
|
13
|
+
Format: postgresql://user:pass@host:port/dbname
|
|
14
|
+
SSL: postgresql://user:pass@host/dbname?ssl=require
|
|
15
|
+
|
|
16
|
+
Optional env vars (all have defaults):
|
|
17
|
+
POOL_SIZE: int = 10
|
|
18
|
+
POOL_MAX_INACTIVE_CONN_LIFETIME: float = 300.0
|
|
19
|
+
POOL_COMMAND_TIMEOUT: float = 60.0
|
|
20
|
+
POOL_MAX_QUERIES: int = 50000
|
|
21
|
+
POSTGRES_ADAPTER_NAME: str = "postgres"
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from openframe.core.config import BaseAdapterSettings
|
|
26
|
+
|
|
27
|
+
__all__ = ["PostgresSettings"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PostgresSettings(BaseAdapterSettings):
|
|
31
|
+
"""
|
|
32
|
+
Settings for the PostgreSQL adapter.
|
|
33
|
+
|
|
34
|
+
All fields read from environment variables. Missing required fields raise
|
|
35
|
+
``pydantic_core.ValidationError`` at instantiation time.
|
|
36
|
+
|
|
37
|
+
Inherits from ``BaseAdapterSettings``:
|
|
38
|
+
adapter_name: str = "postgres" (overrides base default)
|
|
39
|
+
connection_timeout: float = 30.0
|
|
40
|
+
operation_timeout: float = 10.0
|
|
41
|
+
max_retries: int = 3
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
database_url: Full asyncpg DSN (required).
|
|
45
|
+
pool_size: Pool min_size and max_size. Default 10.
|
|
46
|
+
pool_max_inactive_conn_lifetime: Seconds before idle connection is
|
|
47
|
+
closed. Default 300.0.
|
|
48
|
+
pool_command_timeout: Per-statement timeout in the pool.
|
|
49
|
+
Default 60.0.
|
|
50
|
+
pool_max_queries: Queries per connection before recycle.
|
|
51
|
+
Default 50 000.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
database_url: str
|
|
55
|
+
pool_size: int = 10
|
|
56
|
+
pool_max_inactive_conn_lifetime: float = 300.0
|
|
57
|
+
pool_command_timeout: float = 60.0
|
|
58
|
+
pool_max_queries: int = 50_000
|
|
59
|
+
adapter_name: str = "postgres"
|