pyrmute 0.1.0__tar.gz → 0.3.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.
Files changed (43) hide show
  1. {pyrmute-0.1.0 → pyrmute-0.3.0}/.github/workflows/ci.yml +11 -7
  2. {pyrmute-0.1.0 → pyrmute-0.3.0}/.gitignore +2 -0
  3. pyrmute-0.3.0/PKG-INFO +352 -0
  4. pyrmute-0.3.0/README.md +320 -0
  5. {pyrmute-0.1.0 → pyrmute-0.3.0}/pyproject.toml +3 -8
  6. pyrmute-0.3.0/src/pyrmute/__init__.py +47 -0
  7. {pyrmute-0.1.0 → pyrmute-0.3.0}/src/pyrmute/_migration_manager.py +112 -37
  8. {pyrmute-0.1.0 → pyrmute-0.3.0}/src/pyrmute/_registry.py +24 -11
  9. {pyrmute-0.1.0 → pyrmute-0.3.0}/src/pyrmute/_schema_manager.py +31 -34
  10. {pyrmute-0.1.0 → pyrmute-0.3.0}/src/pyrmute/_version.py +3 -3
  11. pyrmute-0.3.0/src/pyrmute/exceptions.py +55 -0
  12. pyrmute-0.3.0/src/pyrmute/migration_testing.py +161 -0
  13. pyrmute-0.3.0/src/pyrmute/model_diff.py +272 -0
  14. pyrmute-0.3.0/src/pyrmute/model_manager.py +725 -0
  15. {pyrmute-0.1.0 → pyrmute-0.3.0}/src/pyrmute/model_version.py +16 -8
  16. {pyrmute-0.1.0 → pyrmute-0.3.0}/src/pyrmute/types.py +17 -5
  17. pyrmute-0.3.0/src/pyrmute.egg-info/PKG-INFO +352 -0
  18. {pyrmute-0.1.0 → pyrmute-0.3.0}/src/pyrmute.egg-info/SOURCES.txt +6 -0
  19. {pyrmute-0.1.0 → pyrmute-0.3.0}/src/pyrmute.egg-info/requires.txt +1 -0
  20. {pyrmute-0.1.0 → pyrmute-0.3.0}/tests/conftest.py +33 -9
  21. pyrmute-0.3.0/tests/test_hypothesis.py +792 -0
  22. pyrmute-0.3.0/tests/test_migration_manager.py +1217 -0
  23. pyrmute-0.3.0/tests/test_migration_testing.py +609 -0
  24. pyrmute-0.3.0/tests/test_model_diff.py +483 -0
  25. pyrmute-0.3.0/tests/test_model_manager.py +2517 -0
  26. {pyrmute-0.1.0 → pyrmute-0.3.0}/tests/test_model_version.py +10 -8
  27. {pyrmute-0.1.0 → pyrmute-0.3.0}/tests/test_registry.py +15 -12
  28. {pyrmute-0.1.0 → pyrmute-0.3.0}/tests/test_schema_manager.py +14 -8
  29. pyrmute-0.3.0/uv.lock +531 -0
  30. pyrmute-0.1.0/PKG-INFO +0 -130
  31. pyrmute-0.1.0/README.md +0 -100
  32. pyrmute-0.1.0/src/pyrmute/__init__.py +0 -20
  33. pyrmute-0.1.0/src/pyrmute/model_manager.py +0 -264
  34. pyrmute-0.1.0/src/pyrmute.egg-info/PKG-INFO +0 -130
  35. pyrmute-0.1.0/tests/test_migration_manager.py +0 -588
  36. pyrmute-0.1.0/tests/test_model_manager.py +0 -553
  37. pyrmute-0.1.0/uv.lock +0 -464
  38. {pyrmute-0.1.0 → pyrmute-0.3.0}/.github/workflows/publish.yml +0 -0
  39. {pyrmute-0.1.0 → pyrmute-0.3.0}/LICENSE +0 -0
  40. {pyrmute-0.1.0 → pyrmute-0.3.0}/setup.cfg +0 -0
  41. {pyrmute-0.1.0 → pyrmute-0.3.0}/src/pyrmute/py.typed +0 -0
  42. {pyrmute-0.1.0 → pyrmute-0.3.0}/src/pyrmute.egg-info/dependency_links.txt +0 -0
  43. {pyrmute-0.1.0 → pyrmute-0.3.0}/src/pyrmute.egg-info/top_level.txt +0 -0
@@ -23,7 +23,7 @@ jobs:
23
23
  runs-on: ubuntu-latest
24
24
  strategy:
25
25
  matrix:
26
- python-version: ["3.11", "3.12", "3.13"]
26
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
27
27
 
28
28
  steps:
29
29
  - name: Checkout
@@ -37,21 +37,25 @@ jobs:
37
37
  - name: Set up Python
38
38
  run: uv python install ${{ matrix.python-version }}
39
39
 
40
- - name: Set up Python
41
- run: uv sync --python ${{ matrix.python-version }} --all-extras --frozen
40
+ - name: Install dependencies
41
+ run: |
42
+ if [ "${{ github.event_name }}" = "schedule" ]; then
43
+ uv sync --all-extras --upgrade
44
+ else
45
+ uv sync --all-extras --frozen
46
+ fi
42
47
 
43
48
  - name: Ruff check
44
- if: ${{ always() }}
45
49
  run: uv run ruff check
46
50
 
47
51
  - name: Ruff format
48
- if: ${{ always() }}
52
+ if: always()
49
53
  run: uv run ruff format --check
50
54
 
51
55
  - name: Check typing with mypy
52
- if: ${{ always() }}
56
+ if: always()
53
57
  run: uv run mypy src tests
54
58
 
55
59
  - name: Run tests
56
- if: ${{ always() }}
60
+ if: always()
57
61
  run: uv run pytest
@@ -6,6 +6,8 @@ dist/
6
6
  wheels/
7
7
  *.egg-info
8
8
 
9
+ .hypothesis
10
+
9
11
  # Virtual environments
10
12
  .venv
11
13
 
pyrmute-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,352 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyrmute
3
+ Version: 0.3.0
4
+ Summary: Pydantic model migrations and schemas
5
+ Author-email: Matt Ferrera <mattferrera@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/mferrera/pyrmute
8
+ Project-URL: Repository, https://github.com/mferrera/pyrmute
9
+ Project-URL: Documentation, https://github.com/mferrera/pyrmute
10
+ Keywords: pydantic,migration,versioning,schema
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Topic :: Utilities
13
+ Classifier: Operating System :: POSIX :: Linux
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Natural Language :: English
19
+ Requires-Python: >=3.11
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: pydantic
23
+ Provides-Extra: dev
24
+ Requires-Dist: hypothesis; extra == "dev"
25
+ Requires-Dist: mypy; extra == "dev"
26
+ Requires-Dist: pytest; extra == "dev"
27
+ Requires-Dist: pytest-cov; extra == "dev"
28
+ Requires-Dist: pytest-mock; extra == "dev"
29
+ Requires-Dist: pytest-xdist; extra == "dev"
30
+ Requires-Dist: ruff; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # pyrmute
34
+ [![ci](https://img.shields.io/github/actions/workflow/status/mferrera/pyrmute/ci.yml?branch=main&logo=github&label=ci)](https://github.com/mferrera/pyrmute/actions?query=event%3Apush+branch%3Amain+workflow%3Aci)
35
+ [![pypi](https://img.shields.io/pypi/v/pyrmute.svg)](https://pypi.python.org/pypi/pyrmute)
36
+ [![versions](https://img.shields.io/pypi/pyversions/pyrmute.svg)](https://github.com/mferrera/pyrmute)
37
+ [![license](https://img.shields.io/github/license/mferrera/pyrmute.svg)](https://github.com/mferrera/pyrmute/blob/main/LICENSE)
38
+
39
+ Pydantic model migrations and schema management with semantic versioning.
40
+
41
+ pyrmute handles the complexity of data model evolution so you can confidently
42
+ make changes without breaking your production systems. Version your models,
43
+ define transformations, and let pyrmute automatically migrate legacy data
44
+ through multiple versions.
45
+
46
+ **Key Features**
47
+
48
+ - **Version your models** - Track schema evolution with semantic versioning
49
+ - **Automatic migration chains** - Transform data across multiple versions
50
+ (1.0.0 → 2.0.0 → 3.0.0) in a single call
51
+ - **Type-safe transformations** - Migrations return validated Pydantic models,
52
+ catching errors before they reach production
53
+ - **Flexible schema export** - Generate JSON schemas for all versions with
54
+ support for `$ref`, custom generators, and nested models
55
+ - **Production-ready** - Batch processing, parallel execution, and streaming
56
+ support for large datasets
57
+ - **Only one dependency** - Pydantic
58
+
59
+ ## Help
60
+
61
+ See [documentation](https://mferrera.github.io/pyrmute/) for complete guides
62
+ and API reference.
63
+
64
+ ## Installation
65
+
66
+ ```bash
67
+ pip install pyrmute
68
+ ```
69
+
70
+ ## Quick Start
71
+
72
+ ```python
73
+ from pydantic import BaseModel
74
+ from pyrmute import ModelManager, ModelData
75
+
76
+ manager = ModelManager()
77
+
78
+
79
+ # Version 1: Simple user model
80
+ @manager.model("User", "1.0.0")
81
+ class UserV1(BaseModel):
82
+ name: str
83
+ age: int
84
+
85
+
86
+ # Version 2: Split name into components
87
+ @manager.model("User", "2.0.0")
88
+ class UserV2(BaseModel):
89
+ first_name: str
90
+ last_name: str
91
+ age: int
92
+
93
+
94
+ # Version 3: Add email and make age optional
95
+ @manager.model("User", "3.0.0")
96
+ class UserV3(BaseModel):
97
+ first_name: str
98
+ last_name: str
99
+ email: str
100
+ age: int | None = None
101
+
102
+
103
+ # Define how to migrate between versions
104
+ @manager.migration("User", "1.0.0", "2.0.0")
105
+ def split_name(data: ModelData) -> ModelData:
106
+ parts = data["name"].split(" ", 1)
107
+ return {
108
+ "first_name": parts[0],
109
+ "last_name": parts[1] if len(parts) > 1 else "",
110
+ "age": data["age"],
111
+ }
112
+
113
+
114
+ @manager.migration("User", "2.0.0", "3.0.0")
115
+ def add_email(data: ModelData) -> ModelData:
116
+ return {
117
+ **data,
118
+ "email": f"{data['first_name'].lower()}@example.com"
119
+ }
120
+
121
+
122
+ # Migrate legacy data to the latest version
123
+ legacy_data = {"name": "John Doe", "age": 30} # or, legacy.model_dump()
124
+ current_user = manager.migrate(legacy_data, "User", "1.0.0", "3.0.0")
125
+
126
+ print(current_user)
127
+ # UserV3(first_name='John', last_name='Doe', email='john@example.com', age=30)
128
+ ```
129
+
130
+ ## Advanced Usage
131
+
132
+ ### Compare Model Versions
133
+
134
+ ```python
135
+ # See exactly what changed between versions
136
+ diff = manager.diff("User", "1.0.0", "3.0.0")
137
+ print(f"Added: {diff.added_fields}")
138
+ print(f"Removed: {diff.removed_fields}")
139
+ # Render a changelog to Markdown
140
+ print(diff.to_markdown(header_depth=4))
141
+ ```
142
+
143
+ With `header_depth=4` the output can be embedded nicely into this document.
144
+
145
+ #### User: 1.0.0 → 3.0.0
146
+
147
+ ##### Added Fields
148
+
149
+ - `email: str` (required)
150
+ - `first_name: str` (required)
151
+ - `last_name: str` (required)
152
+
153
+ ##### Removed Fields
154
+
155
+ - `name`
156
+
157
+ ##### Modified Fields
158
+
159
+ - `age` - type: `int` → `int | None` - now optional - default added: `None`
160
+
161
+ ##### Breaking Changes
162
+
163
+ - ⚠️ New required field 'last_name' will fail for existing data without defaults
164
+ - ⚠️ New required field 'first_name' will fail for existing data without defaults
165
+ - ⚠️ New required field 'email' will fail for existing data without defaults
166
+ - ⚠️ Removed fields 'name' will be lost during migration
167
+ - ⚠️ Field 'age' type changed - may cause validation errors
168
+
169
+
170
+ ### Batch Processing
171
+
172
+ ```python
173
+ # Migrate thousands of records efficiently
174
+ legacy_users = [
175
+ {"name": "Alice Smith", "age": 28},
176
+ {"name": "Bob Johnson", "age": 35},
177
+ # ... thousands more
178
+ ]
179
+
180
+ # Parallel processing for CPU-intensive migrations
181
+ users = manager.migrate_batch(
182
+ legacy_users,
183
+ "User",
184
+ from_version="1.0.0",
185
+ to_version="3.0.0",
186
+ parallel=True,
187
+ max_workers=4,
188
+ )
189
+ ```
190
+
191
+ ### Streaming Large Datasets
192
+
193
+ ```python
194
+ # Process huge datasets without loading everything into memory
195
+ def load_users_from_database() -> Iterator[dict[str, Any]]:
196
+ yield from database.stream_users()
197
+
198
+
199
+ # Migrate and save incrementally
200
+ for user in manager.migrate_batch_streaming(
201
+ load_users_from_database(),
202
+ "User",
203
+ from_version="1.0.0",
204
+ to_version="3.0.0",
205
+ chunk_size=1000
206
+ ):
207
+ database.save(user)
208
+ ```
209
+
210
+ ### Test Your Migrations
211
+
212
+ ```python
213
+ # Validate migration logic with test cases
214
+ results = manager.test_migration(
215
+ "User",
216
+ from_version="1.0.0",
217
+ to_version="2.0.0",
218
+ test_cases=[
219
+ # (input, expected_output)
220
+ (
221
+ {"name": "Alice Smith", "age": 28},
222
+ {"first_name": "Alice", "last_name": "Smith", "age": 28}
223
+ ),
224
+ (
225
+ {"name": "Bob", "age": 35},
226
+ {"first_name": "Bob", "last_name": "", "age": 35}
227
+ ),
228
+ ]
229
+ )
230
+
231
+ # Use in your test suite
232
+ assert results.all_passed, f"Migration failed: {results.failures}"
233
+ ```
234
+
235
+ ### Export JSON Schemas
236
+
237
+ ```python
238
+ # Generate schemas for all versions
239
+ manager.dump_schemas("schemas/")
240
+ # Creates: User_v1.0.0.json, User_v2.0.0.json, User_v3.0.0.json
241
+
242
+ # Use separate files with $ref for nested models with 'enable_ref=True'.
243
+ manager.dump_schemas(
244
+ "schemas/",
245
+ separate_definitions=True,
246
+ ref_template="https://api.example.com/schemas/{model}_v{version}.json"
247
+ )
248
+ ```
249
+
250
+ ### Auto-Migration
251
+
252
+ ```python
253
+ # Skip writing migration functions for simple changes
254
+ @manager.model("Config", "1.0.0")
255
+ class ConfigV1(BaseModel):
256
+ timeout: int = 30
257
+
258
+
259
+ @manager.model("Config", "2.0.0", backward_compatible=True)
260
+ class ConfigV2(BaseModel):
261
+ timeout: int = 30
262
+ retries: int = 3 # New field with default
263
+
264
+
265
+ # No migration function needed - defaults are applied automatically
266
+ config = manager.migrate({"timeout": 60}, "Config", "1.0.0", "2.0.0")
267
+ # ConfigV2(timeout=60, retries=3)
268
+ ```
269
+
270
+ ## Real-World Example
271
+
272
+ ```python
273
+ from datetime import datetime
274
+ from pydantic import BaseModel, EmailStr
275
+ from pyrmute import ModelManager, ModelData
276
+
277
+ manager = ModelManager()
278
+
279
+
280
+ # API v1: Basic order
281
+ @manager.model("Order", "1.0.0")
282
+ class OrderV1(BaseModel):
283
+ id: str
284
+ items: list[str]
285
+ total: float
286
+
287
+
288
+ # API v2: Add customer info
289
+ @manager.model("Order", "2.0.0")
290
+ class OrderV2(BaseModel):
291
+ id: str
292
+ items: list[str]
293
+ total: float
294
+ customer_email: EmailStr
295
+
296
+
297
+ # API v3: Structured items and timestamps
298
+ @manager.model("Order", "3.0.0")
299
+ class OrderItemV3(BaseModel):
300
+ product_id: str
301
+ quantity: int
302
+ price: float
303
+
304
+
305
+ @manager.model("Order", "3.0.0")
306
+ class OrderV3(BaseModel):
307
+ id: str
308
+ items: list[OrderItemV3]
309
+ total: float
310
+ customer_email: EmailStr
311
+ created_at: datetime
312
+
313
+
314
+ # Define migrations
315
+ @manager.migration("Order", "1.0.0", "2.0.0")
316
+ def add_customer_email(data: ModelData) -> ModelData:
317
+ return {**data, "customer_email": "customer@example.com"}
318
+
319
+
320
+ @manager.migration("Order", "2.0.0", "3.0.0")
321
+ def structure_items(data: ModelData) -> ModelData:
322
+ # Convert simple strings to structured items
323
+ structured_items = [
324
+ {
325
+ "product_id": item,
326
+ "quantity": 1,
327
+ "price": data["total"] / len(data["items"])
328
+ }
329
+ for item in data["items"]
330
+ ]
331
+ return {
332
+ **data,
333
+ "items": structured_items,
334
+ "created_at": datetime.now().isoformat()
335
+ }
336
+
337
+ # Migrate old orders from your database
338
+ old_order = {"id": "123", "items": ["widget", "gadget"], "total": 29.99}
339
+ new_order = manager.migrate(old_order, "Order", "1.0.0", "3.0.0")
340
+ database.save(new_order)
341
+ ```
342
+
343
+ ## Contributing
344
+
345
+ For guidance on setting up a development environment and how to make a
346
+ contribution to pyrmute, see [Contributing to
347
+ pyrmute](https://mferrera.github.io/pyrmute/contributing/).
348
+
349
+ ## Reporting a Security Vulnerability
350
+
351
+ See our [security
352
+ policy](https://github.com/mferrera/pyrmute/security/policy).