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