redis-queen 0.2.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.
- redis_queen-0.2.0/.github/workflows/ci.yml +44 -0
- redis_queen-0.2.0/.github/workflows/conventional-commits.yml +15 -0
- redis_queen-0.2.0/.github/workflows/release-please.yml +40 -0
- redis_queen-0.2.0/.gitignore +5 -0
- redis_queen-0.2.0/.pre-commit-config.yaml +21 -0
- redis_queen-0.2.0/.python-version +1 -0
- redis_queen-0.2.0/.release-please-manifest.json +3 -0
- redis_queen-0.2.0/CHANGELOG.md +42 -0
- redis_queen-0.2.0/LICENSE +21 -0
- redis_queen-0.2.0/PKG-INFO +216 -0
- redis_queen-0.2.0/README.md +197 -0
- redis_queen-0.2.0/pyproject.toml +65 -0
- redis_queen-0.2.0/release-please-config.json +18 -0
- redis_queen-0.2.0/src/redis_queen/__init__.py +31 -0
- redis_queen-0.2.0/src/redis_queen/__main__.py +11 -0
- redis_queen-0.2.0/src/redis_queen/_codegen.py +285 -0
- redis_queen-0.2.0/src/redis_queen/_config.py +200 -0
- redis_queen-0.2.0/src/redis_queen/_decorator.py +168 -0
- redis_queen-0.2.0/src/redis_queen/_diff.py +56 -0
- redis_queen-0.2.0/src/redis_queen/_discovery.py +122 -0
- redis_queen-0.2.0/src/redis_queen/_migration.py +219 -0
- redis_queen-0.2.0/src/redis_queen/_revision.py +188 -0
- redis_queen-0.2.0/src/redis_queen/_state.py +37 -0
- redis_queen-0.2.0/src/redis_queen/_types.py +71 -0
- redis_queen-0.2.0/src/redis_queen/_util.py +125 -0
- redis_queen-0.2.0/src/redis_queen/commands/__init__.py +33 -0
- redis_queen-0.2.0/src/redis_queen/commands/diff.py +55 -0
- redis_queen-0.2.0/src/redis_queen/commands/down.py +70 -0
- redis_queen-0.2.0/src/redis_queen/commands/reset.py +97 -0
- redis_queen-0.2.0/src/redis_queen/commands/revision.py +79 -0
- redis_queen-0.2.0/src/redis_queen/commands/show.py +47 -0
- redis_queen-0.2.0/src/redis_queen/commands/up.py +67 -0
- redis_queen-0.2.0/src/redis_queen/py.typed +0 -0
- redis_queen-0.2.0/src/redis_queen/types/__init__.py +27 -0
- redis_queen-0.2.0/tests/__init__.py +0 -0
- redis_queen-0.2.0/tests/conftest.py +55 -0
- redis_queen-0.2.0/tests/test_config.py +140 -0
- redis_queen-0.2.0/tests/test_decorator.py +171 -0
- redis_queen-0.2.0/tests/test_diff.py +106 -0
- redis_queen-0.2.0/tests/test_migration.py +103 -0
- redis_queen-0.2.0/tests/test_revision.py +106 -0
- redis_queen-0.2.0/tests/test_util.py +132 -0
- redis_queen-0.2.0/uv.lock +459 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
ruff:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v6
|
|
15
|
+
- uses: astral-sh/setup-uv@v7
|
|
16
|
+
with:
|
|
17
|
+
enable-cache: true
|
|
18
|
+
- run: uv sync --dev
|
|
19
|
+
- run: uv run ruff check .
|
|
20
|
+
- run: uv run ruff format --check .
|
|
21
|
+
|
|
22
|
+
ty:
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v6
|
|
26
|
+
- uses: astral-sh/setup-uv@v7
|
|
27
|
+
with:
|
|
28
|
+
enable-cache: true
|
|
29
|
+
- run: uv sync --dev
|
|
30
|
+
- run: uv run ty check src/
|
|
31
|
+
|
|
32
|
+
pytest:
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
strategy:
|
|
35
|
+
matrix:
|
|
36
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/checkout@v6
|
|
39
|
+
- uses: astral-sh/setup-uv@v7
|
|
40
|
+
with:
|
|
41
|
+
enable-cache: true
|
|
42
|
+
- run: uv python install ${{ matrix.python-version }}
|
|
43
|
+
- run: uv sync --dev --python ${{ matrix.python-version }}
|
|
44
|
+
- run: uv run --python ${{ matrix.python-version }} pytest
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
name: Conventional Commits
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches: [main]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
conventional-commits:
|
|
9
|
+
if: ${{ !startsWith(github.head_ref, 'release-please--') }}
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v6
|
|
13
|
+
with:
|
|
14
|
+
fetch-depth: 0
|
|
15
|
+
- uses: webiny/action-conventional-commits@v1.3.0
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: Release Please
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: write
|
|
9
|
+
pull-requests: write
|
|
10
|
+
id-token: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
release-please:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
outputs:
|
|
16
|
+
release_created: ${{ steps.release.outputs.release_created }}
|
|
17
|
+
steps:
|
|
18
|
+
- uses: googleapis/release-please-action@v4
|
|
19
|
+
id: release
|
|
20
|
+
with:
|
|
21
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
22
|
+
config-file: release-please-config.json
|
|
23
|
+
manifest-file: .release-please-manifest.json
|
|
24
|
+
|
|
25
|
+
publish:
|
|
26
|
+
needs: release-please
|
|
27
|
+
if: needs.release-please.outputs.release_created == 'true'
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
environment:
|
|
30
|
+
name: pypi
|
|
31
|
+
url: https://pypi.org/p/redis-queen
|
|
32
|
+
permissions:
|
|
33
|
+
id-token: write
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/checkout@v6
|
|
36
|
+
- uses: astral-sh/setup-uv@v7
|
|
37
|
+
with:
|
|
38
|
+
enable-cache: true
|
|
39
|
+
- run: uv build
|
|
40
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: local
|
|
3
|
+
hooks:
|
|
4
|
+
- id: ruff-check
|
|
5
|
+
name: ruff check
|
|
6
|
+
entry: uv run ruff check --fix
|
|
7
|
+
language: system
|
|
8
|
+
types: [python]
|
|
9
|
+
|
|
10
|
+
- id: ruff-format
|
|
11
|
+
name: ruff format
|
|
12
|
+
entry: uv run ruff format
|
|
13
|
+
language: system
|
|
14
|
+
types: [python]
|
|
15
|
+
|
|
16
|
+
- id: ty
|
|
17
|
+
name: ty check
|
|
18
|
+
entry: uv run ty check src/
|
|
19
|
+
language: system
|
|
20
|
+
pass_filenames: false
|
|
21
|
+
files: ^src/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.2.0](https://github.com/mahdilamb/redis-queen/compare/redis-queen-v0.1.2...redis-queen-v0.2.0) (2026-04-12)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* Package name, import module, CLI command, config section, and environment variable have all been renamed.
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* initial release of red-queen ([2c69149](https://github.com/mahdilamb/redis-queen/commit/2c691498b342112aed5f7af48e8cce83cee3b587))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Documentation
|
|
16
|
+
|
|
17
|
+
* add badges to README ([5c2d6d4](https://github.com/mahdilamb/redis-queen/commit/5c2d6d4dcd0f350f359d4f32085d626b8df13c34))
|
|
18
|
+
* add badges, classifiers, and 3.14 to CI matrix ([0b5b397](https://github.com/mahdilamb/redis-queen/commit/0b5b39786ee459576a231232639127f755fcabd0))
|
|
19
|
+
* remove alias-as-rename example from README ([2d3ac74](https://github.com/mahdilamb/redis-queen/commit/2d3ac74838922cb4d74d5026cfd7273796e8315d))
|
|
20
|
+
* remove alias-as-rename example from README ([5b5ecc3](https://github.com/mahdilamb/redis-queen/commit/5b5ecc38d5e40f492924a9cace1315d7fb066b30))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Code Refactoring
|
|
24
|
+
|
|
25
|
+
* rename package from red-queen to redis-queen ([13532d6](https://github.com/mahdilamb/redis-queen/commit/13532d6b6be956f643d5dc4ec1f92f98f3f19019))
|
|
26
|
+
|
|
27
|
+
## [0.1.2](https://github.com/mahdilamb/redis-queen/compare/redis-queen-v0.1.1...redis-queen-v0.1.2) (2026-04-12)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
### Documentation
|
|
31
|
+
|
|
32
|
+
* add badges to README ([5c2d6d4](https://github.com/mahdilamb/redis-queen/commit/5c2d6d4dcd0f350f359d4f32085d626b8df13c34))
|
|
33
|
+
* add badges, classifiers, and 3.14 to CI matrix ([0b5b397](https://github.com/mahdilamb/redis-queen/commit/0b5b39786ee459576a231232639127f755fcabd0))
|
|
34
|
+
* remove alias-as-rename example from README ([2d3ac74](https://github.com/mahdilamb/redis-queen/commit/2d3ac74838922cb4d74d5026cfd7273796e8315d))
|
|
35
|
+
* remove alias-as-rename example from README ([5b5ecc3](https://github.com/mahdilamb/redis-queen/commit/5b5ecc38d5e40f492924a9cace1315d7fb066b30))
|
|
36
|
+
|
|
37
|
+
## [0.1.1](https://github.com/mahdilamb/redis-queen/compare/redis-queen-v0.1.0...redis-queen-v0.1.1) (2026-04-12)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
### Features
|
|
41
|
+
|
|
42
|
+
* initial release of redis-queen ([2c69149](https://github.com/mahdilamb/redis-queen/commit/2c691498b342112aed5f7af48e8cce83cee3b587))
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mahdi Lamb
|
|
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,216 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: redis-queen
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Redis schema migration tool with Pydantic model tracking
|
|
5
|
+
Author-email: Mahdi Lamb <mahdilamb@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Requires-Dist: click>=8.1.0
|
|
15
|
+
Requires-Dist: pydantic>=2.0.0
|
|
16
|
+
Requires-Dist: redis>=7.4.0
|
|
17
|
+
Requires-Dist: tomli>=2.0.0; python_version < '3.11'
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# redis-queen
|
|
21
|
+
|
|
22
|
+
[](https://github.com/mahdilamb/redis-queen/actions/workflows/ci.yml)
|
|
23
|
+
[](https://pypi.org/project/redis-queen/)
|
|
24
|
+
[](https://pypi.org/project/redis-queen/)
|
|
25
|
+
[](LICENSE)
|
|
26
|
+
|
|
27
|
+
Schema migration tool for Redis-backed Pydantic models. Track model changes, generate versioned migration scripts, and apply them using async SCAN-based key iteration.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```toml
|
|
32
|
+
# pyproject.toml
|
|
33
|
+
dependencies = ["redis-queen"]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
### 1. Decorate your models
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from pydantic import BaseModel, Field, RootModel
|
|
42
|
+
from redis_queen import redis_queen, migrates_from
|
|
43
|
+
from typing import Annotated
|
|
44
|
+
|
|
45
|
+
@redis_queen(key="user:{user_id}:profile:")
|
|
46
|
+
class UserProfile(BaseModel):
|
|
47
|
+
name: str
|
|
48
|
+
email: str
|
|
49
|
+
|
|
50
|
+
@redis_queen(key="agent:session:{session_id}:display")
|
|
51
|
+
class DisplayMessages(RootModel[list[DisplayMessage]]):
|
|
52
|
+
"""Stored as a JSON array."""
|
|
53
|
+
|
|
54
|
+
# Field renames
|
|
55
|
+
@redis_queen(key="item:{item_id}")
|
|
56
|
+
class Item(BaseModel):
|
|
57
|
+
title: Annotated[str, migrates_from("name")] # tracks rename from "name"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Key patterns use f-string style placeholders. `match` is auto-derived by replacing `{...}` with `*` for SCAN.
|
|
61
|
+
|
|
62
|
+
### 2. Configure in pyproject.toml
|
|
63
|
+
|
|
64
|
+
```toml
|
|
65
|
+
[tool.redis-queen]
|
|
66
|
+
migrations_dir = "migrations"
|
|
67
|
+
deletion_protection = false # or true, or an integer (TTL in seconds)
|
|
68
|
+
|
|
69
|
+
[tool.redis-queen.profiles.default]
|
|
70
|
+
default = true
|
|
71
|
+
host = "localhost" # defaults to localhost
|
|
72
|
+
port = 6379 # defaults to 6379
|
|
73
|
+
db = 0 # defaults to 0
|
|
74
|
+
migrations_collection = "my_collection"
|
|
75
|
+
model_search_path = ["myapp.models"]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`host`, `port`, and `db` default to `localhost`, `6379`, and `0`. All string values support `$ENV_VAR` expansion.
|
|
79
|
+
|
|
80
|
+
**Profile resolution order:** `--profile` CLI flag > `REDIS_QUEEN_PROFILE` env var > profile with `default = true`.
|
|
81
|
+
|
|
82
|
+
Multiple profiles (e.g. local + docker):
|
|
83
|
+
|
|
84
|
+
```toml
|
|
85
|
+
[tool.redis-queen.profiles.default]
|
|
86
|
+
default = true
|
|
87
|
+
migrations_collection = "agent"
|
|
88
|
+
model_search_path = ["agent.types"]
|
|
89
|
+
|
|
90
|
+
[tool.redis-queen.profiles.docker]
|
|
91
|
+
host = "redis"
|
|
92
|
+
migrations_collection = "agent"
|
|
93
|
+
model_search_path = ["agent.types"]
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 3. Generate and apply migrations
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# Create the initial schema snapshot
|
|
100
|
+
redis-queen revision -m "initial"
|
|
101
|
+
|
|
102
|
+
# Show current state
|
|
103
|
+
redis-queen show
|
|
104
|
+
|
|
105
|
+
# After changing models, generate a new revision
|
|
106
|
+
redis-queen revision -m "add email field"
|
|
107
|
+
|
|
108
|
+
# Check if there are pending changes (exit 1 if none, useful in CI)
|
|
109
|
+
redis-queen revision --check
|
|
110
|
+
|
|
111
|
+
# Show what changed
|
|
112
|
+
redis-queen diff
|
|
113
|
+
|
|
114
|
+
# Apply pending migrations (stages first, prompts for confirmation)
|
|
115
|
+
redis-queen up
|
|
116
|
+
|
|
117
|
+
# Apply without confirmation
|
|
118
|
+
redis-queen up --auto-apply
|
|
119
|
+
|
|
120
|
+
# Downgrade to a specific revision
|
|
121
|
+
redis-queen down 0001
|
|
122
|
+
redis-queen down --root # revert all
|
|
123
|
+
|
|
124
|
+
# Reset to a specific revision (up or down as needed)
|
|
125
|
+
redis-queen reset 0002
|
|
126
|
+
redis-queen reset --root
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
All mutation commands (`up`, `down`, `reset`) support `--auto-apply` to skip the staging confirmation prompt.
|
|
130
|
+
|
|
131
|
+
## Model discovery
|
|
132
|
+
|
|
133
|
+
`model_search_path` accepts:
|
|
134
|
+
|
|
135
|
+
- **Directories:** `"src/myapp/models"` -- walks all `.py` files
|
|
136
|
+
- **Single files:** `"src/myapp/models.py"`
|
|
137
|
+
- **Dotted module paths:** `"myapp.models"` -- imports the module and recursively walks all submodules
|
|
138
|
+
|
|
139
|
+
## Schema tracking
|
|
140
|
+
|
|
141
|
+
Snapshots use `model_json_schema()`, which captures the full recursive schema including nested models. A change to any nested model triggers a new revision.
|
|
142
|
+
|
|
143
|
+
## Generated migrations
|
|
144
|
+
|
|
145
|
+
Revisions auto-generate `upgrade` and `downgrade` functions:
|
|
146
|
+
|
|
147
|
+
- **Field added with default** -- sets the default value
|
|
148
|
+
- **Field added, optional (factory)** -- infers zero-value from type (`[]`, `{}`, `""`, `0`, etc.)
|
|
149
|
+
- **Field added, required, no default** -- `# TODO` comment
|
|
150
|
+
- **Field removed** -- backs up values, deletes from data
|
|
151
|
+
|
|
152
|
+
## Deletion protection
|
|
153
|
+
|
|
154
|
+
Controls what happens when fields are removed:
|
|
155
|
+
|
|
156
|
+
```toml
|
|
157
|
+
[tool.redis-queen]
|
|
158
|
+
deletion_protection = false # backup without TTL (default)
|
|
159
|
+
deletion_protection = true # generate TODO, don't delete
|
|
160
|
+
deletion_protection = 3600 # backup with 1-hour TTL
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
When using an integer TTL, backups expire after the specified seconds. If a downgrade runs after the TTL, a warning is emitted for each key where the backup has expired and fields cannot be restored.
|
|
164
|
+
|
|
165
|
+
## Python API
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from redis_queen import (
|
|
169
|
+
redis_queen,
|
|
170
|
+
migrates_from,
|
|
171
|
+
auto_migrate_up,
|
|
172
|
+
migrate_up,
|
|
173
|
+
migrate_down,
|
|
174
|
+
apply_plan,
|
|
175
|
+
find_one,
|
|
176
|
+
get_one,
|
|
177
|
+
)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Auto-migrate on startup
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
from redis_queen import auto_migrate_up
|
|
184
|
+
|
|
185
|
+
# Resolves config, connects to Redis, applies all pending migrations.
|
|
186
|
+
# Uses REDIS_QUEEN_PROFILE env var or default profile.
|
|
187
|
+
await auto_migrate_up()
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### FastAPI lifespan example
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from redis_queen import auto_migrate_up
|
|
194
|
+
|
|
195
|
+
@asynccontextmanager
|
|
196
|
+
async def lifespan(app: FastAPI):
|
|
197
|
+
await auto_migrate_up()
|
|
198
|
+
yield
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Query utilities
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
# find_one: format key pattern with args/kwargs, then GET
|
|
205
|
+
profile = await find_one(UserProfile, "123")
|
|
206
|
+
profile = await find_one(UserProfile, user_id="123")
|
|
207
|
+
raw = await find_one(UserProfile, "123", return_raw=True)
|
|
208
|
+
|
|
209
|
+
# get_one: GET by full literal key
|
|
210
|
+
profile = await get_one(UserProfile, "user:123:profile:")
|
|
211
|
+
raw = await get_one(UserProfile, "user:123:profile:", return_raw=True)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Low-level API
|
|
215
|
+
|
|
216
|
+
For full control, use `migrate_up` / `migrate_down` directly with an explicit Redis client, migrations dir, and model list. See the CLI command implementations for examples.
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# redis-queen
|
|
2
|
+
|
|
3
|
+
[](https://github.com/mahdilamb/redis-queen/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/redis-queen/)
|
|
5
|
+
[](https://pypi.org/project/redis-queen/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
Schema migration tool for Redis-backed Pydantic models. Track model changes, generate versioned migration scripts, and apply them using async SCAN-based key iteration.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```toml
|
|
13
|
+
# pyproject.toml
|
|
14
|
+
dependencies = ["redis-queen"]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
### 1. Decorate your models
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from pydantic import BaseModel, Field, RootModel
|
|
23
|
+
from redis_queen import redis_queen, migrates_from
|
|
24
|
+
from typing import Annotated
|
|
25
|
+
|
|
26
|
+
@redis_queen(key="user:{user_id}:profile:")
|
|
27
|
+
class UserProfile(BaseModel):
|
|
28
|
+
name: str
|
|
29
|
+
email: str
|
|
30
|
+
|
|
31
|
+
@redis_queen(key="agent:session:{session_id}:display")
|
|
32
|
+
class DisplayMessages(RootModel[list[DisplayMessage]]):
|
|
33
|
+
"""Stored as a JSON array."""
|
|
34
|
+
|
|
35
|
+
# Field renames
|
|
36
|
+
@redis_queen(key="item:{item_id}")
|
|
37
|
+
class Item(BaseModel):
|
|
38
|
+
title: Annotated[str, migrates_from("name")] # tracks rename from "name"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Key patterns use f-string style placeholders. `match` is auto-derived by replacing `{...}` with `*` for SCAN.
|
|
42
|
+
|
|
43
|
+
### 2. Configure in pyproject.toml
|
|
44
|
+
|
|
45
|
+
```toml
|
|
46
|
+
[tool.redis-queen]
|
|
47
|
+
migrations_dir = "migrations"
|
|
48
|
+
deletion_protection = false # or true, or an integer (TTL in seconds)
|
|
49
|
+
|
|
50
|
+
[tool.redis-queen.profiles.default]
|
|
51
|
+
default = true
|
|
52
|
+
host = "localhost" # defaults to localhost
|
|
53
|
+
port = 6379 # defaults to 6379
|
|
54
|
+
db = 0 # defaults to 0
|
|
55
|
+
migrations_collection = "my_collection"
|
|
56
|
+
model_search_path = ["myapp.models"]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`host`, `port`, and `db` default to `localhost`, `6379`, and `0`. All string values support `$ENV_VAR` expansion.
|
|
60
|
+
|
|
61
|
+
**Profile resolution order:** `--profile` CLI flag > `REDIS_QUEEN_PROFILE` env var > profile with `default = true`.
|
|
62
|
+
|
|
63
|
+
Multiple profiles (e.g. local + docker):
|
|
64
|
+
|
|
65
|
+
```toml
|
|
66
|
+
[tool.redis-queen.profiles.default]
|
|
67
|
+
default = true
|
|
68
|
+
migrations_collection = "agent"
|
|
69
|
+
model_search_path = ["agent.types"]
|
|
70
|
+
|
|
71
|
+
[tool.redis-queen.profiles.docker]
|
|
72
|
+
host = "redis"
|
|
73
|
+
migrations_collection = "agent"
|
|
74
|
+
model_search_path = ["agent.types"]
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 3. Generate and apply migrations
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# Create the initial schema snapshot
|
|
81
|
+
redis-queen revision -m "initial"
|
|
82
|
+
|
|
83
|
+
# Show current state
|
|
84
|
+
redis-queen show
|
|
85
|
+
|
|
86
|
+
# After changing models, generate a new revision
|
|
87
|
+
redis-queen revision -m "add email field"
|
|
88
|
+
|
|
89
|
+
# Check if there are pending changes (exit 1 if none, useful in CI)
|
|
90
|
+
redis-queen revision --check
|
|
91
|
+
|
|
92
|
+
# Show what changed
|
|
93
|
+
redis-queen diff
|
|
94
|
+
|
|
95
|
+
# Apply pending migrations (stages first, prompts for confirmation)
|
|
96
|
+
redis-queen up
|
|
97
|
+
|
|
98
|
+
# Apply without confirmation
|
|
99
|
+
redis-queen up --auto-apply
|
|
100
|
+
|
|
101
|
+
# Downgrade to a specific revision
|
|
102
|
+
redis-queen down 0001
|
|
103
|
+
redis-queen down --root # revert all
|
|
104
|
+
|
|
105
|
+
# Reset to a specific revision (up or down as needed)
|
|
106
|
+
redis-queen reset 0002
|
|
107
|
+
redis-queen reset --root
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
All mutation commands (`up`, `down`, `reset`) support `--auto-apply` to skip the staging confirmation prompt.
|
|
111
|
+
|
|
112
|
+
## Model discovery
|
|
113
|
+
|
|
114
|
+
`model_search_path` accepts:
|
|
115
|
+
|
|
116
|
+
- **Directories:** `"src/myapp/models"` -- walks all `.py` files
|
|
117
|
+
- **Single files:** `"src/myapp/models.py"`
|
|
118
|
+
- **Dotted module paths:** `"myapp.models"` -- imports the module and recursively walks all submodules
|
|
119
|
+
|
|
120
|
+
## Schema tracking
|
|
121
|
+
|
|
122
|
+
Snapshots use `model_json_schema()`, which captures the full recursive schema including nested models. A change to any nested model triggers a new revision.
|
|
123
|
+
|
|
124
|
+
## Generated migrations
|
|
125
|
+
|
|
126
|
+
Revisions auto-generate `upgrade` and `downgrade` functions:
|
|
127
|
+
|
|
128
|
+
- **Field added with default** -- sets the default value
|
|
129
|
+
- **Field added, optional (factory)** -- infers zero-value from type (`[]`, `{}`, `""`, `0`, etc.)
|
|
130
|
+
- **Field added, required, no default** -- `# TODO` comment
|
|
131
|
+
- **Field removed** -- backs up values, deletes from data
|
|
132
|
+
|
|
133
|
+
## Deletion protection
|
|
134
|
+
|
|
135
|
+
Controls what happens when fields are removed:
|
|
136
|
+
|
|
137
|
+
```toml
|
|
138
|
+
[tool.redis-queen]
|
|
139
|
+
deletion_protection = false # backup without TTL (default)
|
|
140
|
+
deletion_protection = true # generate TODO, don't delete
|
|
141
|
+
deletion_protection = 3600 # backup with 1-hour TTL
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
When using an integer TTL, backups expire after the specified seconds. If a downgrade runs after the TTL, a warning is emitted for each key where the backup has expired and fields cannot be restored.
|
|
145
|
+
|
|
146
|
+
## Python API
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from redis_queen import (
|
|
150
|
+
redis_queen,
|
|
151
|
+
migrates_from,
|
|
152
|
+
auto_migrate_up,
|
|
153
|
+
migrate_up,
|
|
154
|
+
migrate_down,
|
|
155
|
+
apply_plan,
|
|
156
|
+
find_one,
|
|
157
|
+
get_one,
|
|
158
|
+
)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Auto-migrate on startup
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from redis_queen import auto_migrate_up
|
|
165
|
+
|
|
166
|
+
# Resolves config, connects to Redis, applies all pending migrations.
|
|
167
|
+
# Uses REDIS_QUEEN_PROFILE env var or default profile.
|
|
168
|
+
await auto_migrate_up()
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### FastAPI lifespan example
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
from redis_queen import auto_migrate_up
|
|
175
|
+
|
|
176
|
+
@asynccontextmanager
|
|
177
|
+
async def lifespan(app: FastAPI):
|
|
178
|
+
await auto_migrate_up()
|
|
179
|
+
yield
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Query utilities
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
# find_one: format key pattern with args/kwargs, then GET
|
|
186
|
+
profile = await find_one(UserProfile, "123")
|
|
187
|
+
profile = await find_one(UserProfile, user_id="123")
|
|
188
|
+
raw = await find_one(UserProfile, "123", return_raw=True)
|
|
189
|
+
|
|
190
|
+
# get_one: GET by full literal key
|
|
191
|
+
profile = await get_one(UserProfile, "user:123:profile:")
|
|
192
|
+
raw = await get_one(UserProfile, "user:123:profile:", return_raw=True)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Low-level API
|
|
196
|
+
|
|
197
|
+
For full control, use `migrate_up` / `migrate_down` directly with an explicit Redis client, migrations dir, and model list. See the CLI command implementations for examples.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "redis-queen"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "Redis schema migration tool with Pydantic model tracking"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Mahdi Lamb", email = "mahdilamb@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
license = "MIT"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Programming Language :: Python :: 3.10",
|
|
13
|
+
"Programming Language :: Python :: 3.11",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Programming Language :: Python :: 3.13",
|
|
16
|
+
"Programming Language :: Python :: 3.14",
|
|
17
|
+
]
|
|
18
|
+
dependencies = [
|
|
19
|
+
"click>=8.1.0",
|
|
20
|
+
"pydantic>=2.0.0",
|
|
21
|
+
"redis>=7.4.0",
|
|
22
|
+
"tomli>=2.0.0; python_version < '3.11'",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
redis-queen = "redis_queen.__main__:main"
|
|
27
|
+
|
|
28
|
+
[build-system]
|
|
29
|
+
requires = ["hatchling"]
|
|
30
|
+
build-backend = "hatchling.build"
|
|
31
|
+
|
|
32
|
+
[tool.ruff]
|
|
33
|
+
target-version = "py310"
|
|
34
|
+
extend-exclude = ["**/migrations"]
|
|
35
|
+
|
|
36
|
+
[tool.ruff.lint]
|
|
37
|
+
select = [
|
|
38
|
+
"E", # pycodestyle errors
|
|
39
|
+
"W", # pycodestyle warnings
|
|
40
|
+
"F", # pyflakes
|
|
41
|
+
"I", # isort
|
|
42
|
+
"N", # pep8-naming
|
|
43
|
+
"UP", # pyupgrade
|
|
44
|
+
"B", # flake8-bugbear
|
|
45
|
+
"SIM", # flake8-simplify
|
|
46
|
+
"TCH", # flake8-type-checking
|
|
47
|
+
"RUF", # ruff-specific rules
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[tool.ruff.lint.isort]
|
|
51
|
+
known-first-party = ["redis_queen"]
|
|
52
|
+
|
|
53
|
+
[tool.pytest.ini_options]
|
|
54
|
+
testpaths = ["tests"]
|
|
55
|
+
asyncio_mode = "auto"
|
|
56
|
+
addopts = "--import-mode=importlib"
|
|
57
|
+
|
|
58
|
+
[dependency-groups]
|
|
59
|
+
dev = [
|
|
60
|
+
"fakeredis>=2.35.0",
|
|
61
|
+
"pytest>=9.0.3",
|
|
62
|
+
"pytest-asyncio>=1.3.0",
|
|
63
|
+
"ruff>=0.15.10",
|
|
64
|
+
"ty>=0.0.29",
|
|
65
|
+
]
|