openframe-adapters-db-redis 1.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.
- openframe_adapters_db_redis-1.1.0/.gitignore +221 -0
- openframe_adapters_db_redis-1.1.0/PKG-INFO +122 -0
- openframe_adapters_db_redis-1.1.0/README.md +89 -0
- openframe_adapters_db_redis-1.1.0/openframe/__init__.py +1 -0
- openframe_adapters_db_redis-1.1.0/openframe/adapters/__init__.py +1 -0
- openframe_adapters_db_redis-1.1.0/openframe/adapters/db/__init__.py +1 -0
- openframe_adapters_db_redis-1.1.0/openframe/adapters/db/redis/__init__.py +46 -0
- openframe_adapters_db_redis-1.1.0/openframe/adapters/db/redis/config.py +42 -0
- openframe_adapters_db_redis-1.1.0/openframe/adapters/db/redis/connection.py +94 -0
- openframe_adapters_db_redis-1.1.0/openframe/adapters/db/redis/plugin.py +159 -0
- openframe_adapters_db_redis-1.1.0/openframe/adapters/db/redis/repository.py +429 -0
- openframe_adapters_db_redis-1.1.0/pyproject.toml +58 -0
- openframe_adapters_db_redis-1.1.0/tests/conftest.py +59 -0
- openframe_adapters_db_redis-1.1.0/tests/test_config.py +58 -0
- openframe_adapters_db_redis-1.1.0/tests/test_connection.py +102 -0
- openframe_adapters_db_redis-1.1.0/tests/test_health.py +74 -0
- openframe_adapters_db_redis-1.1.0/tests/test_plugin.py +193 -0
- openframe_adapters_db_redis-1.1.0/tests/test_repository.py +257 -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
|
+
# Miscellaneous
|
|
221
|
+
.DS_Store
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openframe-adapters-db-redis
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: OpenFrame Microservice Suite - Redis key-value adapter.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Furious-Meteors/openframe-adapters
|
|
6
|
+
Project-URL: Documentation, https://furious-meteors.github.io/openframe-adapters/
|
|
7
|
+
Project-URL: Repository, https://github.com/Furious-Meteors/openframe-adapters
|
|
8
|
+
Project-URL: Changelog, https://github.com/Furious-Meteors/openframe-adapters/blob/production/.github/CHANGELOG.md
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/Furious-Meteors/openframe-adapters/issues
|
|
10
|
+
Author-email: Furious Meteors Engineering <engineering@furiousmeteors.dev>
|
|
11
|
+
Maintainer-email: Furious Meteors Engineering <engineering@furiousmeteors.dev>
|
|
12
|
+
License: MIT
|
|
13
|
+
Keywords: cache,hexagonal,microservice,openframe,redis
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Database
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.11
|
|
26
|
+
Requires-Dist: openframe-core<3,>=2.0
|
|
27
|
+
Requires-Dist: redis[asyncio]>=5.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-mock>=3.14; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# openframe-adapters-db-redis
|
|
35
|
+
|
|
36
|
+
Redis key-value adapter for the **OpenFrame Microservice Development Suite**.
|
|
37
|
+
|
|
38
|
+
Part of the [`openframe-adapters`](https://github.com/Furious-Meteors/openframe-adapters) monorepo.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## What it provides
|
|
43
|
+
|
|
44
|
+
| Symbol | Purpose |
|
|
45
|
+
|---|---|
|
|
46
|
+
| `RedisSettings` | Pydantic-settings subclass — reads all config from env vars |
|
|
47
|
+
| `RedisRepository[T]` | Generic async repository — `BaseRepository[T]` + `HealthCheck` |
|
|
48
|
+
| `get_redis_client()` | Async factory — creates and caches the `redis.asyncio.Redis` client |
|
|
49
|
+
| `RedisPlugin` | `OpenFramePlugin` — structured lifecycle via `PluginRegistry` |
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Via meta-package (recommended)
|
|
55
|
+
pip install "openframe-adapters[redis]"
|
|
56
|
+
|
|
57
|
+
# Or directly
|
|
58
|
+
pip install openframe-adapters-db-redis
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Quick start
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from openframe.adapters.db.redis import RedisSettings, RedisRepository
|
|
65
|
+
|
|
66
|
+
settings = RedisSettings(redis_url="redis://localhost:6379/0")
|
|
67
|
+
repo = RedisRepository(settings)
|
|
68
|
+
|
|
69
|
+
# Store and retrieve
|
|
70
|
+
await repo.create({"id": "user-1", "name": "Alice"})
|
|
71
|
+
user = await repo.get("user-1") # {"id": "user-1", "name": "Alice"}
|
|
72
|
+
await repo.update({"id": "user-1", "name": "Alice B."})
|
|
73
|
+
await repo.delete("user-1") # True
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Configuration
|
|
77
|
+
|
|
78
|
+
| Env var | Default | Description |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `REDIS_URL` | **required** | `redis://[:password@]host[:port][/db]` or `rediss://` for TLS |
|
|
81
|
+
| `REDIS_MAX_CONNECTIONS` | `10` | Max connections in pool |
|
|
82
|
+
| `REDIS_SOCKET_TIMEOUT` | `5.0` | Socket read/write timeout (seconds) |
|
|
83
|
+
| `REDIS_SOCKET_CONNECT_TIMEOUT` | `5.0` | Connection timeout (seconds) |
|
|
84
|
+
| `REDIS_KEY_PREFIX` | `"openframe"` | Key namespace — keys stored as `{prefix}:{id}` |
|
|
85
|
+
| `REDIS_DEFAULT_TTL` | `0` | TTL in seconds; `0` = no expiry |
|
|
86
|
+
|
|
87
|
+
## Typed domain objects
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from dataclasses import dataclass
|
|
91
|
+
from openframe.adapters.db.redis import RedisRepository, RedisSettings
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class Session:
|
|
95
|
+
id: str
|
|
96
|
+
user_id: str
|
|
97
|
+
token: str
|
|
98
|
+
|
|
99
|
+
class SessionRepository(RedisRepository[Session]):
|
|
100
|
+
def _dict_to_entity(self, data: dict) -> Session:
|
|
101
|
+
return Session(**data)
|
|
102
|
+
def _entity_to_dict(self, entity: Session) -> dict:
|
|
103
|
+
return {"id": entity.id, "user_id": entity.user_id, "token": entity.token}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Plugin lifecycle (optional)
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from openframe.core.plugins import PluginRegistry
|
|
110
|
+
from openframe.adapters.db.redis import RedisPlugin, RedisSettings
|
|
111
|
+
|
|
112
|
+
registry = PluginRegistry()
|
|
113
|
+
registry.register(RedisPlugin(RedisSettings()))
|
|
114
|
+
await registry.initialize_all()
|
|
115
|
+
|
|
116
|
+
plugin = registry.get("cache") # capability = "cache"
|
|
117
|
+
repo = plugin.get_repository()
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## License
|
|
121
|
+
|
|
122
|
+
MIT — © Furious Meteors Engineering
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# openframe-adapters-db-redis
|
|
2
|
+
|
|
3
|
+
Redis key-value adapter for the **OpenFrame Microservice Development Suite**.
|
|
4
|
+
|
|
5
|
+
Part of the [`openframe-adapters`](https://github.com/Furious-Meteors/openframe-adapters) monorepo.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## What it provides
|
|
10
|
+
|
|
11
|
+
| Symbol | Purpose |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `RedisSettings` | Pydantic-settings subclass — reads all config from env vars |
|
|
14
|
+
| `RedisRepository[T]` | Generic async repository — `BaseRepository[T]` + `HealthCheck` |
|
|
15
|
+
| `get_redis_client()` | Async factory — creates and caches the `redis.asyncio.Redis` client |
|
|
16
|
+
| `RedisPlugin` | `OpenFramePlugin` — structured lifecycle via `PluginRegistry` |
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Via meta-package (recommended)
|
|
22
|
+
pip install "openframe-adapters[redis]"
|
|
23
|
+
|
|
24
|
+
# Or directly
|
|
25
|
+
pip install openframe-adapters-db-redis
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from openframe.adapters.db.redis import RedisSettings, RedisRepository
|
|
32
|
+
|
|
33
|
+
settings = RedisSettings(redis_url="redis://localhost:6379/0")
|
|
34
|
+
repo = RedisRepository(settings)
|
|
35
|
+
|
|
36
|
+
# Store and retrieve
|
|
37
|
+
await repo.create({"id": "user-1", "name": "Alice"})
|
|
38
|
+
user = await repo.get("user-1") # {"id": "user-1", "name": "Alice"}
|
|
39
|
+
await repo.update({"id": "user-1", "name": "Alice B."})
|
|
40
|
+
await repo.delete("user-1") # True
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Configuration
|
|
44
|
+
|
|
45
|
+
| Env var | Default | Description |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `REDIS_URL` | **required** | `redis://[:password@]host[:port][/db]` or `rediss://` for TLS |
|
|
48
|
+
| `REDIS_MAX_CONNECTIONS` | `10` | Max connections in pool |
|
|
49
|
+
| `REDIS_SOCKET_TIMEOUT` | `5.0` | Socket read/write timeout (seconds) |
|
|
50
|
+
| `REDIS_SOCKET_CONNECT_TIMEOUT` | `5.0` | Connection timeout (seconds) |
|
|
51
|
+
| `REDIS_KEY_PREFIX` | `"openframe"` | Key namespace — keys stored as `{prefix}:{id}` |
|
|
52
|
+
| `REDIS_DEFAULT_TTL` | `0` | TTL in seconds; `0` = no expiry |
|
|
53
|
+
|
|
54
|
+
## Typed domain objects
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from dataclasses import dataclass
|
|
58
|
+
from openframe.adapters.db.redis import RedisRepository, RedisSettings
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class Session:
|
|
62
|
+
id: str
|
|
63
|
+
user_id: str
|
|
64
|
+
token: str
|
|
65
|
+
|
|
66
|
+
class SessionRepository(RedisRepository[Session]):
|
|
67
|
+
def _dict_to_entity(self, data: dict) -> Session:
|
|
68
|
+
return Session(**data)
|
|
69
|
+
def _entity_to_dict(self, entity: Session) -> dict:
|
|
70
|
+
return {"id": entity.id, "user_id": entity.user_id, "token": entity.token}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Plugin lifecycle (optional)
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from openframe.core.plugins import PluginRegistry
|
|
77
|
+
from openframe.adapters.db.redis import RedisPlugin, RedisSettings
|
|
78
|
+
|
|
79
|
+
registry = PluginRegistry()
|
|
80
|
+
registry.register(RedisPlugin(RedisSettings()))
|
|
81
|
+
await registry.initialize_all()
|
|
82
|
+
|
|
83
|
+
plugin = registry.get("cache") # capability = "cache"
|
|
84
|
+
repo = plugin.get_repository()
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT — © Furious Meteors Engineering
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
openframe.adapters.db.redis
|
|
3
|
+
=============================
|
|
4
|
+
Redis key-value adapter for the OpenFrame Microservice Suite.
|
|
5
|
+
|
|
6
|
+
Public API::
|
|
7
|
+
|
|
8
|
+
RedisSettings — Pydantic Settings subclass for connection config.
|
|
9
|
+
RedisRepository — Generic async repository (BaseRepository + HealthCheck).
|
|
10
|
+
get_redis_client — Async factory that creates / returns the cached client.
|
|
11
|
+
RedisPlugin — OpenFramePlugin implementation for PluginRegistry.
|
|
12
|
+
|
|
13
|
+
Quick start::
|
|
14
|
+
|
|
15
|
+
from openframe.adapters.db.redis import (
|
|
16
|
+
RedisSettings,
|
|
17
|
+
RedisRepository,
|
|
18
|
+
get_redis_client,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
settings = RedisSettings(redis_url="redis://localhost:6379/0")
|
|
22
|
+
|
|
23
|
+
# Raw dict mode
|
|
24
|
+
repo = RedisRepository(settings)
|
|
25
|
+
item = await repo.get("abc-123") # dict | None
|
|
26
|
+
|
|
27
|
+
# Typed mode — subclass and override mapping methods
|
|
28
|
+
class SessionRepository(RedisRepository[Session]):
|
|
29
|
+
def _dict_to_entity(self, data: dict) -> Session:
|
|
30
|
+
return Session(**data)
|
|
31
|
+
def _entity_to_dict(self, entity: Session) -> dict:
|
|
32
|
+
return entity.model_dump()
|
|
33
|
+
"""
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
from .config import RedisSettings
|
|
37
|
+
from .connection import get_redis_client
|
|
38
|
+
from .plugin import RedisPlugin
|
|
39
|
+
from .repository import RedisRepository
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"RedisSettings",
|
|
43
|
+
"RedisRepository",
|
|
44
|
+
"get_redis_client",
|
|
45
|
+
"RedisPlugin",
|
|
46
|
+
]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
openframe/adapters/db/redis/config.py
|
|
3
|
+
======================================
|
|
4
|
+
Settings for the Redis adapter, sourced from environment variables via
|
|
5
|
+
``openframe-core``'s ``BaseAdapterSettings`` (pydantic-settings).
|
|
6
|
+
|
|
7
|
+
Required env vars:
|
|
8
|
+
REDIS_URL: Redis connection URL.
|
|
9
|
+
Format: redis://[:password@]host[:port][/db]
|
|
10
|
+
TLS: rediss://host:port
|
|
11
|
+
|
|
12
|
+
Optional env vars (all have defaults):
|
|
13
|
+
REDIS_MAX_CONNECTIONS: int = 10
|
|
14
|
+
REDIS_SOCKET_TIMEOUT: float = 5.0
|
|
15
|
+
REDIS_SOCKET_CONNECT_TIMEOUT: float = 5.0
|
|
16
|
+
REDIS_KEY_PREFIX: str = "openframe"
|
|
17
|
+
REDIS_DEFAULT_TTL: int = 0
|
|
18
|
+
ADAPTER_NAME: str = "redis"
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from openframe.core.config import BaseAdapterSettings
|
|
23
|
+
|
|
24
|
+
__all__ = ["RedisSettings"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RedisSettings(BaseAdapterSettings):
|
|
28
|
+
"""
|
|
29
|
+
Pydantic-settings subclass for the Redis adapter.
|
|
30
|
+
|
|
31
|
+
All fields are read from environment variables with the exact names
|
|
32
|
+
shown above. ``BaseAdapterSettings`` provides ``operation_timeout``
|
|
33
|
+
(default 30.0 s) and ``connection_timeout`` (default 30.0 s).
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
redis_url: str
|
|
37
|
+
redis_max_connections: int = 10
|
|
38
|
+
redis_socket_timeout: float = 5.0
|
|
39
|
+
redis_socket_connect_timeout: float = 5.0
|
|
40
|
+
redis_key_prefix: str = "openframe"
|
|
41
|
+
redis_default_ttl: int = 0
|
|
42
|
+
adapter_name: str = "redis"
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
openframe/adapters/db/redis/connection.py
|
|
3
|
+
==========================================
|
|
4
|
+
Async factory that creates and caches a ``redis.asyncio.Redis`` client.
|
|
5
|
+
|
|
6
|
+
The client is shared across all ``RedisRepository`` instances in the same
|
|
7
|
+
process. Cache key is the ``REDIS_URL`` string so different URLs each
|
|
8
|
+
maintain an independent client.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
|
|
14
|
+
import redis.asyncio as aioredis
|
|
15
|
+
import redis.exceptions
|
|
16
|
+
|
|
17
|
+
from openframe.core.exceptions import (
|
|
18
|
+
AdapterConfigurationError,
|
|
19
|
+
AdapterConnectionError,
|
|
20
|
+
AdapterTimeoutError,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from .config import RedisSettings
|
|
24
|
+
|
|
25
|
+
__all__ = ["get_redis_client", "_client_cache"]
|
|
26
|
+
|
|
27
|
+
# Module-level cache: redis_url → redis.asyncio.Redis
|
|
28
|
+
_client_cache: dict[str, aioredis.Redis] = {} # type: ignore[type-arg]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def get_redis_client(settings: RedisSettings) -> aioredis.Redis: # type: ignore[type-arg]
|
|
32
|
+
"""
|
|
33
|
+
Create or return the cached ``redis.asyncio.Redis`` client.
|
|
34
|
+
|
|
35
|
+
Creates the client on first call. Subsequent calls with the same
|
|
36
|
+
``REDIS_URL`` return the cached instance. The client is shared
|
|
37
|
+
across all ``RedisRepository`` instances in the same process.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
settings: A ``RedisSettings`` instance.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
``redis.asyncio.Redis`` — ready to use, with ``decode_responses=True``.
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
AdapterConnectionError: Redis server is unreachable or auth failed.
|
|
47
|
+
AdapterConfigurationError: ``REDIS_URL`` is malformed.
|
|
48
|
+
AdapterTimeoutError: Connection attempt exceeded the timeout.
|
|
49
|
+
"""
|
|
50
|
+
if settings.redis_url in _client_cache:
|
|
51
|
+
return _client_cache[settings.redis_url]
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
async with asyncio.timeout(settings.connection_timeout):
|
|
55
|
+
pool = aioredis.ConnectionPool.from_url(
|
|
56
|
+
settings.redis_url,
|
|
57
|
+
max_connections=settings.redis_max_connections,
|
|
58
|
+
socket_timeout=settings.redis_socket_timeout,
|
|
59
|
+
socket_connect_timeout=settings.redis_socket_connect_timeout,
|
|
60
|
+
decode_responses=True,
|
|
61
|
+
)
|
|
62
|
+
client: aioredis.Redis = aioredis.Redis(connection_pool=pool) # type: ignore[type-arg]
|
|
63
|
+
await client.ping()
|
|
64
|
+
except asyncio.TimeoutError as exc:
|
|
65
|
+
raise AdapterTimeoutError(
|
|
66
|
+
f"Redis connection to {settings.redis_url!r} timed out "
|
|
67
|
+
f"after {settings.connection_timeout}s",
|
|
68
|
+
adapter=settings.adapter_name,
|
|
69
|
+
operation="connect",
|
|
70
|
+
cause=exc,
|
|
71
|
+
) from exc
|
|
72
|
+
except redis.exceptions.AuthenticationError as exc:
|
|
73
|
+
raise AdapterConnectionError(
|
|
74
|
+
f"Redis authentication failed for {settings.redis_url!r}: {exc}",
|
|
75
|
+
adapter=settings.adapter_name,
|
|
76
|
+
operation="connect",
|
|
77
|
+
cause=exc,
|
|
78
|
+
) from exc
|
|
79
|
+
except redis.exceptions.ConnectionError as exc:
|
|
80
|
+
raise AdapterConnectionError(
|
|
81
|
+
f"Redis connection failed for {settings.redis_url!r}: {exc}",
|
|
82
|
+
adapter=settings.adapter_name,
|
|
83
|
+
operation="connect",
|
|
84
|
+
cause=exc,
|
|
85
|
+
) from exc
|
|
86
|
+
except redis.exceptions.ResponseError as exc:
|
|
87
|
+
raise AdapterConfigurationError(
|
|
88
|
+
f"Redis configuration error for {settings.redis_url!r}: {exc}",
|
|
89
|
+
adapter=settings.adapter_name,
|
|
90
|
+
operation="connect",
|
|
91
|
+
) from exc
|
|
92
|
+
|
|
93
|
+
_client_cache[settings.redis_url] = client
|
|
94
|
+
return client
|