pyrmute 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl

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.
@@ -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,17 @@
1
+ pyrmute/__init__.py,sha256=5XCoTvVxwlDF15tgv6pNGYsqRlZ8-t4VvxkobImiY54,1033
2
+ pyrmute/_migration_manager.py,sha256=rdE8QS4LWO-BWEE11TW64whZHsdTEutkBPpKlKY6gS8,12517
3
+ pyrmute/_registry.py,sha256=iUjMPd6CYgyvWT8PxZqHWBZnsHrX25fOPDi_-k_QDJs,6124
4
+ pyrmute/_schema_manager.py,sha256=OFqq96D0A9Wmwmne8QEtzgBYWeqOBGiGecfxFpRVVQ8,11939
5
+ pyrmute/_version.py,sha256=Dg8AmJomLVpjKL6prJylOONZAPRtB86LOce7dorQS_A,704
6
+ pyrmute/exceptions.py,sha256=Q57cUuzzMdkIl5Q0_VyLobpdB0WcrE0ggfC-LBoX2Uo,1681
7
+ pyrmute/migration_testing.py,sha256=ZofbVAgqSIm1WvOovYE6QLdorEtjcU9bYeUZU_9mqlk,4983
8
+ pyrmute/model_diff.py,sha256=vMa2NTYFqt9E7UYDZH4PQmLcoxQw5Sj-nPpUHB_53Ig,9594
9
+ pyrmute/model_manager.py,sha256=BVa8yznbvdOlrHPpBA7PLdDyOUnnwFHxp9O_zUOgjMk,25526
10
+ pyrmute/model_version.py,sha256=ftNDuJlN3S5ZKQK8DKqqwfBDRiz4rGCYn-aJ3n6Zmqk,2025
11
+ pyrmute/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ pyrmute/types.py,sha256=1C5PrgvxUAtGjbsmcYpgOq0BrLr4BgEwWDe4Nl6JhRc,1038
13
+ pyrmute-0.2.0.dist-info/licenses/LICENSE,sha256=otWInySiZeGwhHqQQ7n7nxM5QBSBe2CzeGEmQDZEz8Q,1119
14
+ pyrmute-0.2.0.dist-info/METADATA,sha256=V6qStLPkl95Uuitb4UlFJEFKzxM76mTopmhGlvkKmTs,9437
15
+ pyrmute-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
16
+ pyrmute-0.2.0.dist-info/top_level.txt,sha256=C8QtzqE6yBHkeewSp1QewvsyeHj_VQLYjSa5HLtMiow,8
17
+ pyrmute-0.2.0.dist-info/RECORD,,
@@ -1,130 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: pyrmute
3
- Version: 0.1.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: mypy; extra == "dev"
24
- Requires-Dist: pytest; extra == "dev"
25
- Requires-Dist: pytest-cov; extra == "dev"
26
- Requires-Dist: pytest-mock; extra == "dev"
27
- Requires-Dist: pytest-xdist; extra == "dev"
28
- Requires-Dist: ruff; extra == "dev"
29
- Dynamic: license-file
30
-
31
- # pyrmute
32
-
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/pyrmute/pyrmute.svg)](https://github.com/mferrera/pyrmute/blob/main/LICENSE)
37
-
38
- Pydantic model migrations and schema management with semantic versioning.
39
-
40
- Pyrmute helps you evolve your data models over time without breaking changes.
41
- Version your Pydantic models, define migrations between versions, and
42
- automatically transform legacy data to current schemas. Export JSON schemas
43
- for all versions to maintain API contracts.
44
-
45
- **Key features:**
46
- - **Version your models** - Use semantic versioning to track model evolution
47
- - **Automatic migrations** - Chain migrations across multiple versions (1.0.0 → 2.0.0 → 3.0.0)
48
- - **Validated transformations** - Migrations return validated Pydantic models by default
49
- - **Schema export** - Generate JSON schemas for all versions, with support for `$ref` to external schemas and custom schema generators
50
- - **Nested model support** - Automatically migrates nested Pydantic models
51
-
52
- ## Help
53
-
54
- See [documentation](https://mferrera.github.io/pyrmute/) for more details.
55
-
56
- ## Installation
57
-
58
- Install using `pip install -U pyrmute`.
59
-
60
- ## Simple Example
61
-
62
- ```python
63
- from pydantic import BaseModel
64
- from pyrmute import ModelManager, MigrationData
65
-
66
- manager = ModelManager()
67
-
68
-
69
- @manager.model("User", "1.0.0")
70
- class UserV1(BaseModel):
71
- """Version 1.0.0: Initial user model."""
72
- name: str
73
- age: int
74
-
75
-
76
- @manager.model("User", "2.0.0")
77
- class UserV2(BaseModel):
78
- """Version 2.0.0: Split name into first/last."""
79
- first_name: str
80
- last_name: str
81
- age: int
82
-
83
-
84
- @manager.model("User", "3.0.0")
85
- class UserV3(BaseModel):
86
- """Version 3.0.0: Add email, make age optional."""
87
- first_name: str
88
- last_name: str
89
- email: str
90
- age: int | None = None
91
-
92
-
93
- # Define migrations
94
- @manager.migration("User", "1.0.0", "2.0.0")
95
- def split_name(data: MigrationData) -> MigrationData:
96
- parts = data["name"].split(" ", 1)
97
- return {
98
- "first_name": parts[0],
99
- "last_name": parts[1] if len(parts) > 1 else "",
100
- "age": data["age"],
101
- }
102
-
103
-
104
- @manager.migration("User", "2.0.0", "3.0.0")
105
- def add_email(data: MigrationData) -> MigrationData:
106
- return {**data, "email": f"{data['first_name'].lower()}@example.com"}
107
-
108
-
109
- # Migrate old data forward, from raw data or dumped from the Pydantic model
110
- legacy_data = {"name": "John Doe", "age": 30}
111
- # Returns a validated Pydantic model
112
- current_user = manager.migrate(legacy_data, "User", "1.0.0", "3.0.0")
113
-
114
- print(current_user)
115
- # first_name='John' last_name='Doe' email='john@example.com' age=30
116
-
117
- # Export schemas for all versions
118
- manager.dump_schemas("schemas/")
119
- # Creates: schemas/User_v1.0.0.json, schemas/User_v2.0.0.json, schemas/User_v3.0.0.json
120
- ```
121
-
122
- ## Contributing
123
-
124
- For guidance on setting up a development environment and how to make a
125
- contribution to pyrmute, see
126
- [Contributing to pyrmute](https://mferrera.github.io/pyrmute/contributing/).
127
-
128
- ## Reporting a Security Vulnerability
129
-
130
- See our [security policy](https://github.com/mferrera/pyrmute/security/policy).
@@ -1,14 +0,0 @@
1
- pyrmute/__init__.py,sha256=wGuYwo2F3zPXnB_HLJsmjt7jbZVfCcyVwf0J78KvwzA,516
2
- pyrmute/_migration_manager.py,sha256=Z9Ie-fxJmOsmBw7oGWGHDrcNvr0aANlO6AqnBmJyYSA,9521
3
- pyrmute/_registry.py,sha256=_fZGRQtdkfCsrCrF6GLUuMmHmyq8up9hF6aS9FboQZA,5572
4
- pyrmute/_schema_manager.py,sha256=UdDC9j5nI-ljy9zKnfjhukzM4zfF-Edwu6P7CsdI7Xw,12581
5
- pyrmute/_version.py,sha256=5jwwVncvCiTnhOedfkzzxmxsggwmTBORdFL_4wq0ZeY,704
6
- pyrmute/model_manager.py,sha256=6rh0hXzZAaGv245RpVaAgjIRJ9s5DLmaRbsJAgwGQuQ,8289
7
- pyrmute/model_version.py,sha256=_vyLcIfOzHXrumPqzDSuNZMAZGqFh1ZYv6g933O_aH0,1801
8
- pyrmute/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- pyrmute/types.py,sha256=NmlBA0ohFjpAIyZ6EvhbEigBV645LCZ-k8vaHjtB7Z4,978
10
- pyrmute-0.1.0.dist-info/licenses/LICENSE,sha256=otWInySiZeGwhHqQQ7n7nxM5QBSBe2CzeGEmQDZEz8Q,1119
11
- pyrmute-0.1.0.dist-info/METADATA,sha256=0t3BYtlBAYNORci6prJdVpgzA9Q8A0kjHvTrw8cJCNs,4515
12
- pyrmute-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
13
- pyrmute-0.1.0.dist-info/top_level.txt,sha256=C8QtzqE6yBHkeewSp1QewvsyeHj_VQLYjSa5HLtMiow,8
14
- pyrmute-0.1.0.dist-info/RECORD,,