openframe-adapters-db-mongo 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_mongo-1.0.0/.gitignore +218 -0
- openframe_adapters_db_mongo-1.0.0/PKG-INFO +198 -0
- openframe_adapters_db_mongo-1.0.0/README.md +182 -0
- openframe_adapters_db_mongo-1.0.0/openframe/adapters/db/mongo/__init__.py +49 -0
- openframe_adapters_db_mongo-1.0.0/openframe/adapters/db/mongo/config.py +69 -0
- openframe_adapters_db_mongo-1.0.0/openframe/adapters/db/mongo/connection.py +77 -0
- openframe_adapters_db_mongo-1.0.0/openframe/adapters/db/mongo/repository.py +449 -0
- openframe_adapters_db_mongo-1.0.0/pyproject.toml +31 -0
- openframe_adapters_db_mongo-1.0.0/tests/conftest.py +76 -0
- openframe_adapters_db_mongo-1.0.0/tests/test_config.py +62 -0
- openframe_adapters_db_mongo-1.0.0/tests/test_connection.py +106 -0
- openframe_adapters_db_mongo-1.0.0/tests/test_health.py +103 -0
- openframe_adapters_db_mongo-1.0.0/tests/test_repository.py +405 -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,198 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openframe-adapters-db-mongo
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: OpenFrame Microservice Suite — MongoDB document store adapter.
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: document-store,hexagonal,microservice,mongodb,motor,openframe
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Requires-Dist: motor>=3.3
|
|
9
|
+
Requires-Dist: openframe-core<2,>=1.0
|
|
10
|
+
Requires-Dist: pymongo>=4.6
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest-mock>=3.14; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# openframe-adapters-db-mongo
|
|
18
|
+
|
|
19
|
+
MongoDB document store adapter for the **OpenFrame Microservice Suite**.
|
|
20
|
+
|
|
21
|
+
Part of the `openframe-adapters` monorepo. Implements `BaseRepository[T]` and
|
|
22
|
+
`HealthCheck` from `openframe-core` using `motor` (`AsyncIOMotorClient`).
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install openframe-adapters-db-mongo
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Required env vars:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
MONGO_URL=mongodb://user:password@host:27017
|
|
36
|
+
MONGO_DATABASE=mydb
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Atlas SRV URIs work too:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
MONGO_URL=mongodb+srv://user:password@cluster.mongodb.net
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Quick start
|
|
48
|
+
|
|
49
|
+
### Raw dict mode
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from openframe.adapters.db.mongo import MongoSettings, MongoRepository
|
|
53
|
+
|
|
54
|
+
settings = MongoSettings() # reads MONGO_URL and MONGO_DATABASE from env
|
|
55
|
+
repo = MongoRepository(settings, collection="artifacts")
|
|
56
|
+
|
|
57
|
+
doc = await repo.get("507f1f77bcf86cd799439011") # dict | None
|
|
58
|
+
docs, total = await repo.list(10, 0) # ([dict, ...], int)
|
|
59
|
+
created = await repo.create({"title": "paper"})
|
|
60
|
+
updated = await repo.update({"_id": "...", "title": "updated"})
|
|
61
|
+
deleted = await repo.delete("507f1f77bcf86cd799439011") # bool
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Typed domain mode
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from dataclasses import dataclass
|
|
68
|
+
from openframe.adapters.db.mongo import MongoSettings, MongoRepository
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class Artifact:
|
|
72
|
+
id: str
|
|
73
|
+
title: str
|
|
74
|
+
artifact_type: str
|
|
75
|
+
|
|
76
|
+
class ArtifactRepository(MongoRepository[Artifact]):
|
|
77
|
+
_collection = "artifacts"
|
|
78
|
+
|
|
79
|
+
def _doc_to_entity(self, doc: dict) -> Artifact:
|
|
80
|
+
return Artifact(
|
|
81
|
+
id=doc["_id"],
|
|
82
|
+
title=doc["title"],
|
|
83
|
+
artifact_type=doc["artifact_type"],
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def _entity_to_doc(self, entity: Artifact) -> dict:
|
|
87
|
+
return {
|
|
88
|
+
"_id": entity.id,
|
|
89
|
+
"title": entity.title,
|
|
90
|
+
"artifact_type": entity.artifact_type,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
settings = MongoSettings()
|
|
94
|
+
repo = ArtifactRepository(settings)
|
|
95
|
+
artifact: Artifact | None = await repo.get("507f1f77bcf86cd799439011")
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## `_id` handling
|
|
101
|
+
|
|
102
|
+
MongoDB uses `_id` as the document identifier; `BaseRepository` uses
|
|
103
|
+
`entity_id: str`. The adapter bridges this transparently:
|
|
104
|
+
|
|
105
|
+
- **Reading:** `_id` is always returned as `str` — `ObjectId` is never
|
|
106
|
+
exposed to callers. A convenience `id` key is also added mirroring `_id`.
|
|
107
|
+
- **Writing:** If an entity dict has an `id` key but no `_id` key, the
|
|
108
|
+
adapter maps `id` → `_id` before insertion.
|
|
109
|
+
- **Filtering:** String entity IDs that are valid 24-char hex are parsed
|
|
110
|
+
as `ObjectId` for indexed lookups; other strings are used as plain string
|
|
111
|
+
`_id` values.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Configuration
|
|
116
|
+
|
|
117
|
+
All settings are read from environment variables.
|
|
118
|
+
|
|
119
|
+
| Env var | Type | Default | Description |
|
|
120
|
+
|---|---|---|---|
|
|
121
|
+
| `MONGO_URL` | `str` | **required** | Motor/pymongo connection string |
|
|
122
|
+
| `MONGO_DATABASE` | `str` | **required** | Database name |
|
|
123
|
+
| `MONGO_MIN_POOL_SIZE` | `int` | `5` | Minimum pool connections |
|
|
124
|
+
| `MONGO_MAX_POOL_SIZE` | `int` | `20` | Maximum pool connections |
|
|
125
|
+
| `MONGO_SERVER_SELECTION_TIMEOUT_MS` | `int` | `5000` | Server selection timeout (ms) |
|
|
126
|
+
| `MONGO_TLS` | `bool` | `False` | Enable TLS/SSL |
|
|
127
|
+
| `MONGO_TLS_ALLOW_INVALID_CERTS` | `bool` | `False` | Skip cert validation (dev only) |
|
|
128
|
+
| `CONNECTION_TIMEOUT` | `float` | `30.0` | Pool creation timeout (s) |
|
|
129
|
+
| `OPERATION_TIMEOUT` | `float` | `10.0` | Per-operation timeout (s) |
|
|
130
|
+
| `MAX_RETRIES` | `int` | `3` | Max retry attempts |
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Timeout strategy
|
|
135
|
+
|
|
136
|
+
Two layers of timeout on every operation:
|
|
137
|
+
|
|
138
|
+
1. `asyncio.timeout(settings.operation_timeout)` — cancels the Python coroutine.
|
|
139
|
+
2. `max_time_ms=int(settings.operation_timeout * 1000)` passed to motor query
|
|
140
|
+
methods — instructs the MongoDB server to abort the query.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Health checks
|
|
145
|
+
|
|
146
|
+
`MongoRepository` implements the `HealthCheck` protocol from `openframe-core`.
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
alive = await repo.ping() # admin.command("ping") — fast liveness check
|
|
150
|
+
ready = await repo.is_ready() # list_collection_names() — full readiness check
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Both methods return `False` on any failure and never raise.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Exception hierarchy
|
|
158
|
+
|
|
159
|
+
All exceptions are `AdapterError` subclasses from `openframe.core.exceptions`.
|
|
160
|
+
Raw motor/pymongo exceptions never escape the adapter.
|
|
161
|
+
|
|
162
|
+
| Situation | Exception |
|
|
163
|
+
|---|---|
|
|
164
|
+
| Cannot connect to MongoDB | `AdapterConnectionError` |
|
|
165
|
+
| Invalid `MONGO_URL` syntax | `AdapterConfigurationError` |
|
|
166
|
+
| Query failed (constraint, auth, etc.) | `AdapterQueryError` |
|
|
167
|
+
| Operation exceeded timeout | `AdapterTimeoutError` |
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Development
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# from the package directory
|
|
175
|
+
pip install -e ".[dev]"
|
|
176
|
+
pytest tests/ -v
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Protocol conformance
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
from openframe.core.ports import BaseRepository
|
|
185
|
+
from openframe.core.health import HealthCheck
|
|
186
|
+
|
|
187
|
+
repo = MongoRepository(settings, collection="artifacts")
|
|
188
|
+
assert isinstance(repo, BaseRepository) # True — structural check
|
|
189
|
+
assert isinstance(repo, HealthCheck) # True — structural check
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
No inheritance from either Protocol is required or used.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## License
|
|
197
|
+
|
|
198
|
+
MIT
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# openframe-adapters-db-mongo
|
|
2
|
+
|
|
3
|
+
MongoDB document store adapter for the **OpenFrame Microservice Suite**.
|
|
4
|
+
|
|
5
|
+
Part of the `openframe-adapters` monorepo. Implements `BaseRepository[T]` and
|
|
6
|
+
`HealthCheck` from `openframe-core` using `motor` (`AsyncIOMotorClient`).
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install openframe-adapters-db-mongo
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Required env vars:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
MONGO_URL=mongodb://user:password@host:27017
|
|
20
|
+
MONGO_DATABASE=mydb
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Atlas SRV URIs work too:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
MONGO_URL=mongodb+srv://user:password@cluster.mongodb.net
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Quick start
|
|
32
|
+
|
|
33
|
+
### Raw dict mode
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from openframe.adapters.db.mongo import MongoSettings, MongoRepository
|
|
37
|
+
|
|
38
|
+
settings = MongoSettings() # reads MONGO_URL and MONGO_DATABASE from env
|
|
39
|
+
repo = MongoRepository(settings, collection="artifacts")
|
|
40
|
+
|
|
41
|
+
doc = await repo.get("507f1f77bcf86cd799439011") # dict | None
|
|
42
|
+
docs, total = await repo.list(10, 0) # ([dict, ...], int)
|
|
43
|
+
created = await repo.create({"title": "paper"})
|
|
44
|
+
updated = await repo.update({"_id": "...", "title": "updated"})
|
|
45
|
+
deleted = await repo.delete("507f1f77bcf86cd799439011") # bool
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Typed domain mode
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from dataclasses import dataclass
|
|
52
|
+
from openframe.adapters.db.mongo import MongoSettings, MongoRepository
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class Artifact:
|
|
56
|
+
id: str
|
|
57
|
+
title: str
|
|
58
|
+
artifact_type: str
|
|
59
|
+
|
|
60
|
+
class ArtifactRepository(MongoRepository[Artifact]):
|
|
61
|
+
_collection = "artifacts"
|
|
62
|
+
|
|
63
|
+
def _doc_to_entity(self, doc: dict) -> Artifact:
|
|
64
|
+
return Artifact(
|
|
65
|
+
id=doc["_id"],
|
|
66
|
+
title=doc["title"],
|
|
67
|
+
artifact_type=doc["artifact_type"],
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def _entity_to_doc(self, entity: Artifact) -> dict:
|
|
71
|
+
return {
|
|
72
|
+
"_id": entity.id,
|
|
73
|
+
"title": entity.title,
|
|
74
|
+
"artifact_type": entity.artifact_type,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
settings = MongoSettings()
|
|
78
|
+
repo = ArtifactRepository(settings)
|
|
79
|
+
artifact: Artifact | None = await repo.get("507f1f77bcf86cd799439011")
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## `_id` handling
|
|
85
|
+
|
|
86
|
+
MongoDB uses `_id` as the document identifier; `BaseRepository` uses
|
|
87
|
+
`entity_id: str`. The adapter bridges this transparently:
|
|
88
|
+
|
|
89
|
+
- **Reading:** `_id` is always returned as `str` — `ObjectId` is never
|
|
90
|
+
exposed to callers. A convenience `id` key is also added mirroring `_id`.
|
|
91
|
+
- **Writing:** If an entity dict has an `id` key but no `_id` key, the
|
|
92
|
+
adapter maps `id` → `_id` before insertion.
|
|
93
|
+
- **Filtering:** String entity IDs that are valid 24-char hex are parsed
|
|
94
|
+
as `ObjectId` for indexed lookups; other strings are used as plain string
|
|
95
|
+
`_id` values.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Configuration
|
|
100
|
+
|
|
101
|
+
All settings are read from environment variables.
|
|
102
|
+
|
|
103
|
+
| Env var | Type | Default | Description |
|
|
104
|
+
|---|---|---|---|
|
|
105
|
+
| `MONGO_URL` | `str` | **required** | Motor/pymongo connection string |
|
|
106
|
+
| `MONGO_DATABASE` | `str` | **required** | Database name |
|
|
107
|
+
| `MONGO_MIN_POOL_SIZE` | `int` | `5` | Minimum pool connections |
|
|
108
|
+
| `MONGO_MAX_POOL_SIZE` | `int` | `20` | Maximum pool connections |
|
|
109
|
+
| `MONGO_SERVER_SELECTION_TIMEOUT_MS` | `int` | `5000` | Server selection timeout (ms) |
|
|
110
|
+
| `MONGO_TLS` | `bool` | `False` | Enable TLS/SSL |
|
|
111
|
+
| `MONGO_TLS_ALLOW_INVALID_CERTS` | `bool` | `False` | Skip cert validation (dev only) |
|
|
112
|
+
| `CONNECTION_TIMEOUT` | `float` | `30.0` | Pool creation timeout (s) |
|
|
113
|
+
| `OPERATION_TIMEOUT` | `float` | `10.0` | Per-operation timeout (s) |
|
|
114
|
+
| `MAX_RETRIES` | `int` | `3` | Max retry attempts |
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Timeout strategy
|
|
119
|
+
|
|
120
|
+
Two layers of timeout on every operation:
|
|
121
|
+
|
|
122
|
+
1. `asyncio.timeout(settings.operation_timeout)` — cancels the Python coroutine.
|
|
123
|
+
2. `max_time_ms=int(settings.operation_timeout * 1000)` passed to motor query
|
|
124
|
+
methods — instructs the MongoDB server to abort the query.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Health checks
|
|
129
|
+
|
|
130
|
+
`MongoRepository` implements the `HealthCheck` protocol from `openframe-core`.
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
alive = await repo.ping() # admin.command("ping") — fast liveness check
|
|
134
|
+
ready = await repo.is_ready() # list_collection_names() — full readiness check
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Both methods return `False` on any failure and never raise.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Exception hierarchy
|
|
142
|
+
|
|
143
|
+
All exceptions are `AdapterError` subclasses from `openframe.core.exceptions`.
|
|
144
|
+
Raw motor/pymongo exceptions never escape the adapter.
|
|
145
|
+
|
|
146
|
+
| Situation | Exception |
|
|
147
|
+
|---|---|
|
|
148
|
+
| Cannot connect to MongoDB | `AdapterConnectionError` |
|
|
149
|
+
| Invalid `MONGO_URL` syntax | `AdapterConfigurationError` |
|
|
150
|
+
| Query failed (constraint, auth, etc.) | `AdapterQueryError` |
|
|
151
|
+
| Operation exceeded timeout | `AdapterTimeoutError` |
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Development
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
# from the package directory
|
|
159
|
+
pip install -e ".[dev]"
|
|
160
|
+
pytest tests/ -v
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Protocol conformance
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from openframe.core.ports import BaseRepository
|
|
169
|
+
from openframe.core.health import HealthCheck
|
|
170
|
+
|
|
171
|
+
repo = MongoRepository(settings, collection="artifacts")
|
|
172
|
+
assert isinstance(repo, BaseRepository) # True — structural check
|
|
173
|
+
assert isinstance(repo, HealthCheck) # True — structural check
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
No inheritance from either Protocol is required or used.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
MIT
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
openframe.adapters.db.mongo
|
|
3
|
+
=============================
|
|
4
|
+
MongoDB document store adapter for the OpenFrame Microservice Suite.
|
|
5
|
+
|
|
6
|
+
Public API:
|
|
7
|
+
|
|
8
|
+
MongoSettings — Pydantic Settings subclass for connection config.
|
|
9
|
+
MongoRepository — Generic async repository (BaseRepository + HealthCheck).
|
|
10
|
+
get_mongo_client — Synchronous factory that creates / returns the cached client.
|
|
11
|
+
|
|
12
|
+
Quick start::
|
|
13
|
+
|
|
14
|
+
from openframe.adapters.db.mongo import (
|
|
15
|
+
MongoSettings,
|
|
16
|
+
MongoRepository,
|
|
17
|
+
get_mongo_client,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
settings = MongoSettings(
|
|
21
|
+
mongo_url="mongodb://user:pw@localhost:27017",
|
|
22
|
+
mongo_database="mydb",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Raw dict mode
|
|
26
|
+
repo = MongoRepository(settings, collection="artifacts")
|
|
27
|
+
doc = await repo.get("507f1f77bcf86cd799439011") # dict | None
|
|
28
|
+
|
|
29
|
+
# Typed mode — subclass and override mapping methods
|
|
30
|
+
class ArtifactRepository(MongoRepository[Artifact]):
|
|
31
|
+
_collection = "artifacts"
|
|
32
|
+
|
|
33
|
+
def _doc_to_entity(self, doc):
|
|
34
|
+
return Artifact(**doc)
|
|
35
|
+
|
|
36
|
+
def _entity_to_doc(self, entity):
|
|
37
|
+
return entity.model_dump()
|
|
38
|
+
"""
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
from .config import MongoSettings
|
|
42
|
+
from .connection import get_mongo_client
|
|
43
|
+
from .repository import MongoRepository
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"MongoSettings",
|
|
47
|
+
"MongoRepository",
|
|
48
|
+
"get_mongo_client",
|
|
49
|
+
]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
openframe/adapters/db/mongo/config.py
|
|
3
|
+
=======================================
|
|
4
|
+
MongoDB 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
|
+
MONGO_URL: Full motor/pymongo connection string.
|
|
13
|
+
Standard: mongodb://user:pass@host:port
|
|
14
|
+
Atlas: mongodb+srv://user:pass@cluster.mongodb.net
|
|
15
|
+
Atlas SRV URIs require dnspython, which motor resolves
|
|
16
|
+
automatically — no extra dependency needed.
|
|
17
|
+
MONGO_DATABASE: Database name to operate on.
|
|
18
|
+
|
|
19
|
+
Optional env vars (all have defaults):
|
|
20
|
+
MONGO_MIN_POOL_SIZE: int = 5
|
|
21
|
+
MONGO_MAX_POOL_SIZE: int = 20
|
|
22
|
+
MONGO_SERVER_SELECTION_TIMEOUT_MS: int = 5000
|
|
23
|
+
MONGO_TLS: bool = False
|
|
24
|
+
MONGO_TLS_ALLOW_INVALID_CERTS: bool = False
|
|
25
|
+
ADAPTER_NAME: str = "mongo"
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from openframe.core.config import BaseAdapterSettings
|
|
30
|
+
|
|
31
|
+
__all__ = ["MongoSettings"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MongoSettings(BaseAdapterSettings):
|
|
35
|
+
"""
|
|
36
|
+
Settings for the MongoDB adapter.
|
|
37
|
+
|
|
38
|
+
All fields read from environment variables. Missing required fields raise
|
|
39
|
+
``pydantic_core.ValidationError`` at instantiation time.
|
|
40
|
+
|
|
41
|
+
Inherits from ``BaseAdapterSettings``:
|
|
42
|
+
adapter_name: str = "mongo" (overrides base default)
|
|
43
|
+
connection_timeout: float = 30.0
|
|
44
|
+
operation_timeout: float = 10.0
|
|
45
|
+
max_retries: int = 3
|
|
46
|
+
|
|
47
|
+
Attributes:
|
|
48
|
+
mongo_url: Motor/pymongo connection string
|
|
49
|
+
(required). Standard or Atlas SRV.
|
|
50
|
+
mongo_database: Database name (required).
|
|
51
|
+
mongo_min_pool_size: Minimum connections in the pool.
|
|
52
|
+
Default 5.
|
|
53
|
+
mongo_max_pool_size: Maximum connections in the pool.
|
|
54
|
+
Default 20.
|
|
55
|
+
mongo_server_selection_timeout_ms: Milliseconds to wait for a suitable
|
|
56
|
+
server. Default 5000.
|
|
57
|
+
mongo_tls: Enable TLS/SSL. Default False.
|
|
58
|
+
mongo_tls_allow_invalid_certs: Skip certificate validation — for
|
|
59
|
+
development only. Default False.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
mongo_url: str
|
|
63
|
+
mongo_database: str
|
|
64
|
+
mongo_min_pool_size: int = 5
|
|
65
|
+
mongo_max_pool_size: int = 20
|
|
66
|
+
mongo_server_selection_timeout_ms: int = 5000
|
|
67
|
+
mongo_tls: bool = False
|
|
68
|
+
mongo_tls_allow_invalid_certs: bool = False
|
|
69
|
+
adapter_name: str = "mongo"
|