pyrmute 0.2.0__tar.gz → 0.4.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.2.0 → pyrmute-0.4.0}/.github/workflows/ci.yml +11 -7
  2. {pyrmute-0.2.0 → pyrmute-0.4.0}/PKG-INFO +229 -70
  3. pyrmute-0.4.0/README.md +474 -0
  4. pyrmute-0.4.0/examples/advanced_features.py +373 -0
  5. pyrmute-0.4.0/examples/config_file_migration.py +448 -0
  6. pyrmute-0.4.0/examples/etl_data_import.py +714 -0
  7. pyrmute-0.4.0/examples/message_queue_consumer.py +534 -0
  8. pyrmute-0.4.0/examples/ml_inference_pipeline.py +671 -0
  9. {pyrmute-0.2.0 → pyrmute-0.4.0}/pyproject.toml +1 -0
  10. {pyrmute-0.2.0 → pyrmute-0.4.0}/src/pyrmute/__init__.py +13 -11
  11. pyrmute-0.4.0/src/pyrmute/_migration_manager.py +766 -0
  12. {pyrmute-0.2.0 → pyrmute-0.4.0}/src/pyrmute/_schema_manager.py +17 -7
  13. {pyrmute-0.2.0 → pyrmute-0.4.0}/src/pyrmute/_version.py +3 -3
  14. {pyrmute-0.2.0 → pyrmute-0.4.0}/src/pyrmute/migration_testing.py +8 -5
  15. {pyrmute-0.2.0 → pyrmute-0.4.0}/src/pyrmute/model_manager.py +46 -66
  16. {pyrmute-0.2.0 → pyrmute-0.4.0}/src/pyrmute/types.py +11 -4
  17. {pyrmute-0.2.0 → pyrmute-0.4.0}/src/pyrmute.egg-info/PKG-INFO +229 -70
  18. {pyrmute-0.2.0 → pyrmute-0.4.0}/src/pyrmute.egg-info/SOURCES.txt +5 -0
  19. {pyrmute-0.2.0 → pyrmute-0.4.0}/tests/conftest.py +2 -2
  20. {pyrmute-0.2.0 → pyrmute-0.4.0}/tests/test_hypothesis.py +23 -23
  21. pyrmute-0.4.0/tests/test_migration_manager.py +3316 -0
  22. {pyrmute-0.2.0 → pyrmute-0.4.0}/tests/test_migration_testing.py +5 -5
  23. {pyrmute-0.2.0 → pyrmute-0.4.0}/tests/test_model_manager.py +24 -80
  24. {pyrmute-0.2.0 → pyrmute-0.4.0}/tests/test_schema_manager.py +11 -7
  25. {pyrmute-0.2.0 → pyrmute-0.4.0}/uv.lock +146 -112
  26. pyrmute-0.2.0/README.md +0 -316
  27. pyrmute-0.2.0/src/pyrmute/_migration_manager.py +0 -381
  28. pyrmute-0.2.0/tests/test_migration_manager.py +0 -1217
  29. {pyrmute-0.2.0 → pyrmute-0.4.0}/.github/workflows/publish.yml +0 -0
  30. {pyrmute-0.2.0 → pyrmute-0.4.0}/.gitignore +0 -0
  31. {pyrmute-0.2.0 → pyrmute-0.4.0}/LICENSE +0 -0
  32. {pyrmute-0.2.0 → pyrmute-0.4.0}/setup.cfg +0 -0
  33. {pyrmute-0.2.0 → pyrmute-0.4.0}/src/pyrmute/_registry.py +0 -0
  34. {pyrmute-0.2.0 → pyrmute-0.4.0}/src/pyrmute/exceptions.py +0 -0
  35. {pyrmute-0.2.0 → pyrmute-0.4.0}/src/pyrmute/model_diff.py +0 -0
  36. {pyrmute-0.2.0 → pyrmute-0.4.0}/src/pyrmute/model_version.py +0 -0
  37. {pyrmute-0.2.0 → pyrmute-0.4.0}/src/pyrmute/py.typed +0 -0
  38. {pyrmute-0.2.0 → pyrmute-0.4.0}/src/pyrmute.egg-info/dependency_links.txt +0 -0
  39. {pyrmute-0.2.0 → pyrmute-0.4.0}/src/pyrmute.egg-info/requires.txt +0 -0
  40. {pyrmute-0.2.0 → pyrmute-0.4.0}/src/pyrmute.egg-info/top_level.txt +0 -0
  41. {pyrmute-0.2.0 → pyrmute-0.4.0}/tests/test_model_diff.py +0 -0
  42. {pyrmute-0.2.0 → pyrmute-0.4.0}/tests/test_model_version.py +0 -0
  43. {pyrmute-0.2.0 → pyrmute-0.4.0}/tests/test_registry.py +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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyrmute
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: Pydantic model migrations and schemas
5
5
  Author-email: Matt Ferrera <mattferrera@gmail.com>
6
6
  License: MIT
@@ -14,6 +14,7 @@ Classifier: Operating System :: POSIX :: Linux
14
14
  Classifier: Programming Language :: Python :: 3.11
15
15
  Classifier: Programming Language :: Python :: 3.12
16
16
  Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
17
18
  Classifier: Natural Language :: English
18
19
  Requires-Python: >=3.11
19
20
  Description-Content-Type: text/markdown
@@ -55,6 +56,28 @@ through multiple versions.
55
56
  support for large datasets
56
57
  - **Only one dependency** - Pydantic
57
58
 
59
+ ## When to Use pyrmute
60
+
61
+ pyrmute excels at handling schema evolution in production systems:
62
+
63
+ - **Configuration files** - Upgrade user config files as your CLI/desktop app
64
+ evolves (`.apprc`, `config.json`, `settings.yaml`)
65
+ - **Message queues & event streams** - Handle messages from multiple service
66
+ versions publishing different schemas (Kafka, RabbitMQ, SQS)
67
+ - **ETL & data imports** - Import CSV/JSON/Excel files exported over years
68
+ with evolving structures
69
+ - **ML model serving** - Manage feature schema evolution across model versions
70
+ and A/B tests
71
+ - **API versioning** - Support multiple API versions with automatic
72
+ request/response migration
73
+ - **Database migrations** - Transparently migrate legacy data on read without
74
+ downtime
75
+ - **Data archival** - Process historical data dumps with various schema
76
+ versions
77
+
78
+ See the [examples/](examples/) directory for complete, runnable code
79
+ demonstrating these patterns.
80
+
58
81
  ## Help
59
82
 
60
83
  See [documentation](https://mferrera.github.io/pyrmute/) for complete guides
@@ -70,7 +93,7 @@ pip install pyrmute
70
93
 
71
94
  ```python
72
95
  from pydantic import BaseModel
73
- from pyrmute import ModelManager, MigrationData
96
+ from pyrmute import ModelManager, ModelData
74
97
 
75
98
  manager = ModelManager()
76
99
 
@@ -101,7 +124,7 @@ class UserV3(BaseModel):
101
124
 
102
125
  # Define how to migrate between versions
103
126
  @manager.migration("User", "1.0.0", "2.0.0")
104
- def split_name(data: MigrationData) -> MigrationData:
127
+ def split_name(data: ModelData) -> ModelData:
105
128
  parts = data["name"].split(" ", 1)
106
129
  return {
107
130
  "first_name": parts[0],
@@ -111,7 +134,7 @@ def split_name(data: MigrationData) -> MigrationData:
111
134
 
112
135
 
113
136
  @manager.migration("User", "2.0.0", "3.0.0")
114
- def add_email(data: MigrationData) -> MigrationData:
137
+ def add_email(data: ModelData) -> ModelData:
115
138
  return {
116
139
  **data,
117
140
  "email": f"{data['first_name'].lower()}@example.com"
@@ -133,6 +156,9 @@ print(current_user)
133
156
  ```python
134
157
  # See exactly what changed between versions
135
158
  diff = manager.diff("User", "1.0.0", "3.0.0")
159
+ print(f"Added: {diff.added_fields}")
160
+ print(f"Removed: {diff.removed_fields}")
161
+ # Render a changelog to Markdown
136
162
  print(diff.to_markdown(header_depth=4))
137
163
  ```
138
164
 
@@ -228,6 +254,71 @@ results = manager.test_migration(
228
254
  assert results.all_passed, f"Migration failed: {results.failures}"
229
255
  ```
230
256
 
257
+ ### Bidirectional Migrations
258
+
259
+ ```python
260
+ # Support both upgrades and downgrades
261
+ @manager.migration("Config", "2.0.0", "1.0.0")
262
+ def downgrade_config(data: ModelData) -> ModelData:
263
+ """Rollback to v1 format."""
264
+ return {k: v for k, v in data.items() if k in ["setting1", "setting2"]}
265
+
266
+ # Useful for:
267
+ # - Rolling back deployments
268
+ # - Normalizing outputs from multiple model versions
269
+ # - Supporting legacy systems during transitions
270
+ ```
271
+
272
+ ### Nested Model Migrations
273
+
274
+ ```python
275
+ # Automatically migrates nested Pydantic models
276
+ @manager.model("Address", "1.0.0")
277
+ class AddressV1(BaseModel):
278
+ street: str
279
+ city: str
280
+
281
+ @manager.model("Address", "2.0.0")
282
+ class AddressV2(BaseModel):
283
+ street: str
284
+ city: str
285
+ postal_code: str
286
+
287
+ @manager.model("User", "2.0.0")
288
+ class UserV2(BaseModel):
289
+ name: str
290
+ address: AddressV2 # Nested model
291
+
292
+ # When migrating User, Address is automatically migrated too
293
+ @manager.migration("Address", "1.0.0", "2.0.0")
294
+ def add_postal_code(data: ModelData) -> ModelData:
295
+ return {**data, "postal_code": "00000"}
296
+ ```
297
+
298
+ ### Discriminated Unions
299
+
300
+ ```python
301
+ from typing import Literal, Union
302
+ from pydantic import Field
303
+
304
+ # Handle complex type hierarchies
305
+ @manager.model("CreditCard", "1.0.0")
306
+ class CreditCardV1(BaseModel):
307
+ type: Literal["credit_card"] = "credit_card"
308
+ card_number: str
309
+
310
+ @manager.model("PayPal", "1.0.0")
311
+ class PayPalV1(BaseModel):
312
+ type: Literal["paypal"] = "paypal"
313
+ email: str
314
+
315
+ @manager.model("Payment", "1.0.0")
316
+ class PaymentV1(BaseModel):
317
+ method: Union[CreditCardV1, PayPalV1] = Field(discriminator="type")
318
+
319
+ # Migrations respect discriminated unions
320
+ ```
321
+
231
322
  ### Export JSON Schemas
232
323
 
233
324
  ```python
@@ -236,8 +327,9 @@ manager.dump_schemas("schemas/")
236
327
  # Creates: User_v1.0.0.json, User_v2.0.0.json, User_v3.0.0.json
237
328
 
238
329
  # Use separate files with $ref for nested models with 'enable_ref=True'.
239
- manager.dump_schemas_with_refs(
330
+ manager.dump_schemas(
240
331
  "schemas/",
332
+ separate_definitions=True,
241
333
  ref_template="https://api.example.com/schemas/{model}_v{version}.json"
242
334
  )
243
335
  ```
@@ -262,79 +354,146 @@ config = manager.migrate({"timeout": 60}, "Config", "1.0.0", "2.0.0")
262
354
  # ConfigV2(timeout=60, retries=3)
263
355
  ```
264
356
 
265
- ## Real-World Example
357
+ ## Real-World Examples
358
+
359
+ ### Configuration File Evolution
266
360
 
267
361
  ```python
268
- from datetime import datetime
269
- from pydantic import BaseModel, EmailStr
270
- from pyrmute import ModelManager, MigrationData
362
+ # Your CLI tool evolves over time
363
+ @manager.model("AppConfig", "1.0.0")
364
+ class AppConfigV1(BaseModel):
365
+ api_key: str
366
+ debug: bool = False
367
+
368
+ @manager.model("AppConfig", "2.0.0")
369
+ class AppConfigV2(BaseModel):
370
+ api_key: str
371
+ api_endpoint: str = "https://api.example.com"
372
+ log_level: Literal["DEBUG", "INFO", "ERROR"] = "INFO"
373
+
374
+ @manager.migration("AppConfig", "1.0.0", "2.0.0")
375
+ def upgrade_config(data: dict) -> dict:
376
+ return {
377
+ "api_key": data["api_key"],
378
+ "api_endpoint": "https://api.example.com",
379
+ "log_level": "DEBUG" if data.get("debug") else "INFO",
380
+ }
271
381
 
272
- manager = ModelManager()
382
+ # Load and auto-upgrade user's config file
383
+ def load_config(config_path: Path) -> AppConfigV2:
384
+ with open(config_path) as f:
385
+ data = json.load(f)
273
386
 
387
+ version = data.get("_version", "1.0.0")
274
388
 
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
- }
389
+ # Migrate to current version
390
+ config = manager.migrate(
391
+ data,
392
+ "AppConfig",
393
+ from_version=version,
394
+ to_version="2.0.0"
395
+ )
331
396
 
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)
397
+ # Save upgraded config with version tag
398
+ with open(config_path, "w") as f:
399
+ json.dump({**config.model_dump(), "_version": "2.0.0"}, f, indent=2)
400
+
401
+ return config
336
402
  ```
337
403
 
404
+ ### Message Queue Consumer
405
+
406
+ ```python
407
+ # Handle messages from multiple service versions
408
+ @manager.model("OrderEvent", "1.0.0")
409
+ class OrderEventV1(BaseModel):
410
+ order_id: str
411
+ customer_email: str
412
+ items: list[dict] # Unstructured
413
+
414
+ @manager.model("OrderEvent", "2.0.0")
415
+ class OrderEventV2(BaseModel):
416
+ order_id: str
417
+ customer_email: str
418
+ items: list[OrderItem] # Structured
419
+ total: Decimal
420
+
421
+ def process_message(message: dict, schema_version: str) -> None:
422
+ # Migrate to current schema regardless of source version
423
+ event = manager.migrate(
424
+ message,
425
+ "OrderEvent",
426
+ from_version=schema_version,
427
+ to_version="2.0.0"
428
+ )
429
+ # Process with current schema only
430
+ fulfill_order(event)
431
+ ```
432
+
433
+ ### ETL Data Import
434
+
435
+ ```python
436
+ # Import historical exports with evolving schemas
437
+ import csv
438
+
439
+ def import_customers(file_path: Path, file_version: str) -> None:
440
+ with open(file_path) as f:
441
+ reader = csv.DictReader(f)
442
+
443
+ # Stream migration for memory efficiency
444
+ for customer in manager.migrate_batch_streaming(
445
+ reader,
446
+ "Customer",
447
+ from_version=file_version,
448
+ to_version="3.0.0",
449
+ chunk_size=1000
450
+ ):
451
+ database.save(customer)
452
+
453
+ # Handle files from different years
454
+ import_customers("exports/2022_customers.csv", "1.0.0")
455
+ import_customers("exports/2023_customers.csv", "2.0.0")
456
+ import_customers("exports/2024_customers.csv", "3.0.0")
457
+ ```
458
+
459
+ ### ML Model Serving
460
+
461
+ ```python
462
+ # Route requests to appropriate model versions
463
+ class InferenceService:
464
+ def predict(self, features: dict, request_version: str) -> BaseModel:
465
+ # Determine target model version (A/B testing, gradual rollout, etc.)
466
+ model_version = self.get_model_version(features["user_id"])
467
+
468
+ # Migrate request to model's expected format
469
+ model_input = manager.migrate(
470
+ features,
471
+ "PredictionRequest",
472
+ from_version=request_version,
473
+ to_version=model_version
474
+ )
475
+
476
+ # Run inference
477
+ prediction = self.models[model_version].predict(model_input)
478
+
479
+ # Normalize output for logging/analytics
480
+ return manager.migrate(
481
+ prediction,
482
+ "PredictionResponse",
483
+ from_version=model_version,
484
+ to_version="3.0.0"
485
+ )
486
+ ```
487
+
488
+ See [examples/](examples/) for complete runnable code:
489
+ - `config_file_migration.py` - CLI/desktop app config file evolution
490
+ - `message_queue_consumer.py` - Kafka/RabbitMQ/SQS consumer handling multiple
491
+ schemas
492
+ - `etl_data_import.py` - CSV/JSON/Excel import pipeline with historical data
493
+ - `ml_inference_pipeline.py` - ML model serving with feature evolution
494
+ - `advanced_features.py` - Complex Pydantic features (unions, nested models,
495
+ validators)
496
+
338
497
  ## Contributing
339
498
 
340
499
  For guidance on setting up a development environment and how to make a