pyrmute 0.3.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.
- {pyrmute-0.3.0 → pyrmute-0.4.0}/PKG-INFO +220 -66
- {pyrmute-0.3.0 → pyrmute-0.4.0}/README.md +219 -65
- pyrmute-0.4.0/examples/advanced_features.py +373 -0
- pyrmute-0.4.0/examples/config_file_migration.py +448 -0
- pyrmute-0.4.0/examples/etl_data_import.py +714 -0
- pyrmute-0.4.0/examples/message_queue_consumer.py +534 -0
- pyrmute-0.4.0/examples/ml_inference_pipeline.py +671 -0
- {pyrmute-0.3.0 → pyrmute-0.4.0}/src/pyrmute/__init__.py +2 -0
- pyrmute-0.4.0/src/pyrmute/_migration_manager.py +766 -0
- {pyrmute-0.3.0 → pyrmute-0.4.0}/src/pyrmute/_version.py +3 -3
- {pyrmute-0.3.0 → pyrmute-0.4.0}/src/pyrmute/migration_testing.py +4 -1
- {pyrmute-0.3.0 → pyrmute-0.4.0}/src/pyrmute/model_manager.py +2 -1
- {pyrmute-0.3.0 → pyrmute-0.4.0}/src/pyrmute.egg-info/PKG-INFO +220 -66
- {pyrmute-0.3.0 → pyrmute-0.4.0}/src/pyrmute.egg-info/SOURCES.txt +5 -0
- pyrmute-0.4.0/tests/test_migration_manager.py +3316 -0
- pyrmute-0.3.0/src/pyrmute/_migration_manager.py +0 -381
- pyrmute-0.3.0/tests/test_migration_manager.py +0 -1217
- {pyrmute-0.3.0 → pyrmute-0.4.0}/.github/workflows/ci.yml +0 -0
- {pyrmute-0.3.0 → pyrmute-0.4.0}/.github/workflows/publish.yml +0 -0
- {pyrmute-0.3.0 → pyrmute-0.4.0}/.gitignore +0 -0
- {pyrmute-0.3.0 → pyrmute-0.4.0}/LICENSE +0 -0
- {pyrmute-0.3.0 → pyrmute-0.4.0}/pyproject.toml +0 -0
- {pyrmute-0.3.0 → pyrmute-0.4.0}/setup.cfg +0 -0
- {pyrmute-0.3.0 → pyrmute-0.4.0}/src/pyrmute/_registry.py +0 -0
- {pyrmute-0.3.0 → pyrmute-0.4.0}/src/pyrmute/_schema_manager.py +0 -0
- {pyrmute-0.3.0 → pyrmute-0.4.0}/src/pyrmute/exceptions.py +0 -0
- {pyrmute-0.3.0 → pyrmute-0.4.0}/src/pyrmute/model_diff.py +0 -0
- {pyrmute-0.3.0 → pyrmute-0.4.0}/src/pyrmute/model_version.py +0 -0
- {pyrmute-0.3.0 → pyrmute-0.4.0}/src/pyrmute/py.typed +0 -0
- {pyrmute-0.3.0 → pyrmute-0.4.0}/src/pyrmute/types.py +0 -0
- {pyrmute-0.3.0 → pyrmute-0.4.0}/src/pyrmute.egg-info/dependency_links.txt +0 -0
- {pyrmute-0.3.0 → pyrmute-0.4.0}/src/pyrmute.egg-info/requires.txt +0 -0
- {pyrmute-0.3.0 → pyrmute-0.4.0}/src/pyrmute.egg-info/top_level.txt +0 -0
- {pyrmute-0.3.0 → pyrmute-0.4.0}/tests/conftest.py +0 -0
- {pyrmute-0.3.0 → pyrmute-0.4.0}/tests/test_hypothesis.py +0 -0
- {pyrmute-0.3.0 → pyrmute-0.4.0}/tests/test_migration_testing.py +0 -0
- {pyrmute-0.3.0 → pyrmute-0.4.0}/tests/test_model_diff.py +0 -0
- {pyrmute-0.3.0 → pyrmute-0.4.0}/tests/test_model_manager.py +0 -0
- {pyrmute-0.3.0 → pyrmute-0.4.0}/tests/test_model_version.py +0 -0
- {pyrmute-0.3.0 → pyrmute-0.4.0}/tests/test_registry.py +0 -0
- {pyrmute-0.3.0 → pyrmute-0.4.0}/tests/test_schema_manager.py +0 -0
- {pyrmute-0.3.0 → pyrmute-0.4.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: pyrmute
|
3
|
-
Version: 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
|
@@ -56,6 +56,28 @@ through multiple versions.
|
|
56
56
|
support for large datasets
|
57
57
|
- **Only one dependency** - Pydantic
|
58
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
|
+
|
59
81
|
## Help
|
60
82
|
|
61
83
|
See [documentation](https://mferrera.github.io/pyrmute/) for complete guides
|
@@ -232,6 +254,71 @@ results = manager.test_migration(
|
|
232
254
|
assert results.all_passed, f"Migration failed: {results.failures}"
|
233
255
|
```
|
234
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
|
+
|
235
322
|
### Export JSON Schemas
|
236
323
|
|
237
324
|
```python
|
@@ -267,79 +354,146 @@ config = manager.migrate({"timeout": 60}, "Config", "1.0.0", "2.0.0")
|
|
267
354
|
# ConfigV2(timeout=60, retries=3)
|
268
355
|
```
|
269
356
|
|
270
|
-
## Real-World
|
357
|
+
## Real-World Examples
|
358
|
+
|
359
|
+
### Configuration File Evolution
|
271
360
|
|
272
361
|
```python
|
273
|
-
|
274
|
-
|
275
|
-
|
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
|
+
}
|
276
381
|
|
277
|
-
|
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)
|
278
386
|
|
387
|
+
version = data.get("_version", "1.0.0")
|
279
388
|
|
280
|
-
#
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
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
|
-
}
|
389
|
+
# Migrate to current version
|
390
|
+
config = manager.migrate(
|
391
|
+
data,
|
392
|
+
"AppConfig",
|
393
|
+
from_version=version,
|
394
|
+
to_version="2.0.0"
|
395
|
+
)
|
336
396
|
|
337
|
-
#
|
338
|
-
|
339
|
-
|
340
|
-
|
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
|
341
402
|
```
|
342
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
|
+
|
343
497
|
## Contributing
|
344
498
|
|
345
499
|
For guidance on setting up a development environment and how to make a
|
@@ -24,6 +24,28 @@ through multiple versions.
|
|
24
24
|
support for large datasets
|
25
25
|
- **Only one dependency** - Pydantic
|
26
26
|
|
27
|
+
## When to Use pyrmute
|
28
|
+
|
29
|
+
pyrmute excels at handling schema evolution in production systems:
|
30
|
+
|
31
|
+
- **Configuration files** - Upgrade user config files as your CLI/desktop app
|
32
|
+
evolves (`.apprc`, `config.json`, `settings.yaml`)
|
33
|
+
- **Message queues & event streams** - Handle messages from multiple service
|
34
|
+
versions publishing different schemas (Kafka, RabbitMQ, SQS)
|
35
|
+
- **ETL & data imports** - Import CSV/JSON/Excel files exported over years
|
36
|
+
with evolving structures
|
37
|
+
- **ML model serving** - Manage feature schema evolution across model versions
|
38
|
+
and A/B tests
|
39
|
+
- **API versioning** - Support multiple API versions with automatic
|
40
|
+
request/response migration
|
41
|
+
- **Database migrations** - Transparently migrate legacy data on read without
|
42
|
+
downtime
|
43
|
+
- **Data archival** - Process historical data dumps with various schema
|
44
|
+
versions
|
45
|
+
|
46
|
+
See the [examples/](examples/) directory for complete, runnable code
|
47
|
+
demonstrating these patterns.
|
48
|
+
|
27
49
|
## Help
|
28
50
|
|
29
51
|
See [documentation](https://mferrera.github.io/pyrmute/) for complete guides
|
@@ -200,6 +222,71 @@ results = manager.test_migration(
|
|
200
222
|
assert results.all_passed, f"Migration failed: {results.failures}"
|
201
223
|
```
|
202
224
|
|
225
|
+
### Bidirectional Migrations
|
226
|
+
|
227
|
+
```python
|
228
|
+
# Support both upgrades and downgrades
|
229
|
+
@manager.migration("Config", "2.0.0", "1.0.0")
|
230
|
+
def downgrade_config(data: ModelData) -> ModelData:
|
231
|
+
"""Rollback to v1 format."""
|
232
|
+
return {k: v for k, v in data.items() if k in ["setting1", "setting2"]}
|
233
|
+
|
234
|
+
# Useful for:
|
235
|
+
# - Rolling back deployments
|
236
|
+
# - Normalizing outputs from multiple model versions
|
237
|
+
# - Supporting legacy systems during transitions
|
238
|
+
```
|
239
|
+
|
240
|
+
### Nested Model Migrations
|
241
|
+
|
242
|
+
```python
|
243
|
+
# Automatically migrates nested Pydantic models
|
244
|
+
@manager.model("Address", "1.0.0")
|
245
|
+
class AddressV1(BaseModel):
|
246
|
+
street: str
|
247
|
+
city: str
|
248
|
+
|
249
|
+
@manager.model("Address", "2.0.0")
|
250
|
+
class AddressV2(BaseModel):
|
251
|
+
street: str
|
252
|
+
city: str
|
253
|
+
postal_code: str
|
254
|
+
|
255
|
+
@manager.model("User", "2.0.0")
|
256
|
+
class UserV2(BaseModel):
|
257
|
+
name: str
|
258
|
+
address: AddressV2 # Nested model
|
259
|
+
|
260
|
+
# When migrating User, Address is automatically migrated too
|
261
|
+
@manager.migration("Address", "1.0.0", "2.0.0")
|
262
|
+
def add_postal_code(data: ModelData) -> ModelData:
|
263
|
+
return {**data, "postal_code": "00000"}
|
264
|
+
```
|
265
|
+
|
266
|
+
### Discriminated Unions
|
267
|
+
|
268
|
+
```python
|
269
|
+
from typing import Literal, Union
|
270
|
+
from pydantic import Field
|
271
|
+
|
272
|
+
# Handle complex type hierarchies
|
273
|
+
@manager.model("CreditCard", "1.0.0")
|
274
|
+
class CreditCardV1(BaseModel):
|
275
|
+
type: Literal["credit_card"] = "credit_card"
|
276
|
+
card_number: str
|
277
|
+
|
278
|
+
@manager.model("PayPal", "1.0.0")
|
279
|
+
class PayPalV1(BaseModel):
|
280
|
+
type: Literal["paypal"] = "paypal"
|
281
|
+
email: str
|
282
|
+
|
283
|
+
@manager.model("Payment", "1.0.0")
|
284
|
+
class PaymentV1(BaseModel):
|
285
|
+
method: Union[CreditCardV1, PayPalV1] = Field(discriminator="type")
|
286
|
+
|
287
|
+
# Migrations respect discriminated unions
|
288
|
+
```
|
289
|
+
|
203
290
|
### Export JSON Schemas
|
204
291
|
|
205
292
|
```python
|
@@ -235,79 +322,146 @@ config = manager.migrate({"timeout": 60}, "Config", "1.0.0", "2.0.0")
|
|
235
322
|
# ConfigV2(timeout=60, retries=3)
|
236
323
|
```
|
237
324
|
|
238
|
-
## Real-World
|
325
|
+
## Real-World Examples
|
326
|
+
|
327
|
+
### Configuration File Evolution
|
328
|
+
|
329
|
+
```python
|
330
|
+
# Your CLI tool evolves over time
|
331
|
+
@manager.model("AppConfig", "1.0.0")
|
332
|
+
class AppConfigV1(BaseModel):
|
333
|
+
api_key: str
|
334
|
+
debug: bool = False
|
335
|
+
|
336
|
+
@manager.model("AppConfig", "2.0.0")
|
337
|
+
class AppConfigV2(BaseModel):
|
338
|
+
api_key: str
|
339
|
+
api_endpoint: str = "https://api.example.com"
|
340
|
+
log_level: Literal["DEBUG", "INFO", "ERROR"] = "INFO"
|
341
|
+
|
342
|
+
@manager.migration("AppConfig", "1.0.0", "2.0.0")
|
343
|
+
def upgrade_config(data: dict) -> dict:
|
344
|
+
return {
|
345
|
+
"api_key": data["api_key"],
|
346
|
+
"api_endpoint": "https://api.example.com",
|
347
|
+
"log_level": "DEBUG" if data.get("debug") else "INFO",
|
348
|
+
}
|
349
|
+
|
350
|
+
# Load and auto-upgrade user's config file
|
351
|
+
def load_config(config_path: Path) -> AppConfigV2:
|
352
|
+
with open(config_path) as f:
|
353
|
+
data = json.load(f)
|
354
|
+
|
355
|
+
version = data.get("_version", "1.0.0")
|
356
|
+
|
357
|
+
# Migrate to current version
|
358
|
+
config = manager.migrate(
|
359
|
+
data,
|
360
|
+
"AppConfig",
|
361
|
+
from_version=version,
|
362
|
+
to_version="2.0.0"
|
363
|
+
)
|
364
|
+
|
365
|
+
# Save upgraded config with version tag
|
366
|
+
with open(config_path, "w") as f:
|
367
|
+
json.dump({**config.model_dump(), "_version": "2.0.0"}, f, indent=2)
|
368
|
+
|
369
|
+
return config
|
370
|
+
```
|
371
|
+
|
372
|
+
### Message Queue Consumer
|
239
373
|
|
240
374
|
```python
|
241
|
-
from
|
242
|
-
|
243
|
-
|
375
|
+
# Handle messages from multiple service versions
|
376
|
+
@manager.model("OrderEvent", "1.0.0")
|
377
|
+
class OrderEventV1(BaseModel):
|
378
|
+
order_id: str
|
379
|
+
customer_email: str
|
380
|
+
items: list[dict] # Unstructured
|
381
|
+
|
382
|
+
@manager.model("OrderEvent", "2.0.0")
|
383
|
+
class OrderEventV2(BaseModel):
|
384
|
+
order_id: str
|
385
|
+
customer_email: str
|
386
|
+
items: list[OrderItem] # Structured
|
387
|
+
total: Decimal
|
388
|
+
|
389
|
+
def process_message(message: dict, schema_version: str) -> None:
|
390
|
+
# Migrate to current schema regardless of source version
|
391
|
+
event = manager.migrate(
|
392
|
+
message,
|
393
|
+
"OrderEvent",
|
394
|
+
from_version=schema_version,
|
395
|
+
to_version="2.0.0"
|
396
|
+
)
|
397
|
+
# Process with current schema only
|
398
|
+
fulfill_order(event)
|
399
|
+
```
|
244
400
|
|
245
|
-
|
401
|
+
### ETL Data Import
|
246
402
|
|
403
|
+
```python
|
404
|
+
# Import historical exports with evolving schemas
|
405
|
+
import csv
|
406
|
+
|
407
|
+
def import_customers(file_path: Path, file_version: str) -> None:
|
408
|
+
with open(file_path) as f:
|
409
|
+
reader = csv.DictReader(f)
|
410
|
+
|
411
|
+
# Stream migration for memory efficiency
|
412
|
+
for customer in manager.migrate_batch_streaming(
|
413
|
+
reader,
|
414
|
+
"Customer",
|
415
|
+
from_version=file_version,
|
416
|
+
to_version="3.0.0",
|
417
|
+
chunk_size=1000
|
418
|
+
):
|
419
|
+
database.save(customer)
|
420
|
+
|
421
|
+
# Handle files from different years
|
422
|
+
import_customers("exports/2022_customers.csv", "1.0.0")
|
423
|
+
import_customers("exports/2023_customers.csv", "2.0.0")
|
424
|
+
import_customers("exports/2024_customers.csv", "3.0.0")
|
425
|
+
```
|
247
426
|
|
248
|
-
|
249
|
-
@manager.model("Order", "1.0.0")
|
250
|
-
class OrderV1(BaseModel):
|
251
|
-
id: str
|
252
|
-
items: list[str]
|
253
|
-
total: float
|
254
|
-
|
255
|
-
|
256
|
-
# API v2: Add customer info
|
257
|
-
@manager.model("Order", "2.0.0")
|
258
|
-
class OrderV2(BaseModel):
|
259
|
-
id: str
|
260
|
-
items: list[str]
|
261
|
-
total: float
|
262
|
-
customer_email: EmailStr
|
263
|
-
|
264
|
-
|
265
|
-
# API v3: Structured items and timestamps
|
266
|
-
@manager.model("Order", "3.0.0")
|
267
|
-
class OrderItemV3(BaseModel):
|
268
|
-
product_id: str
|
269
|
-
quantity: int
|
270
|
-
price: float
|
271
|
-
|
272
|
-
|
273
|
-
@manager.model("Order", "3.0.0")
|
274
|
-
class OrderV3(BaseModel):
|
275
|
-
id: str
|
276
|
-
items: list[OrderItemV3]
|
277
|
-
total: float
|
278
|
-
customer_email: EmailStr
|
279
|
-
created_at: datetime
|
280
|
-
|
281
|
-
|
282
|
-
# Define migrations
|
283
|
-
@manager.migration("Order", "1.0.0", "2.0.0")
|
284
|
-
def add_customer_email(data: ModelData) -> ModelData:
|
285
|
-
return {**data, "customer_email": "customer@example.com"}
|
286
|
-
|
287
|
-
|
288
|
-
@manager.migration("Order", "2.0.0", "3.0.0")
|
289
|
-
def structure_items(data: ModelData) -> ModelData:
|
290
|
-
# Convert simple strings to structured items
|
291
|
-
structured_items = [
|
292
|
-
{
|
293
|
-
"product_id": item,
|
294
|
-
"quantity": 1,
|
295
|
-
"price": data["total"] / len(data["items"])
|
296
|
-
}
|
297
|
-
for item in data["items"]
|
298
|
-
]
|
299
|
-
return {
|
300
|
-
**data,
|
301
|
-
"items": structured_items,
|
302
|
-
"created_at": datetime.now().isoformat()
|
303
|
-
}
|
427
|
+
### ML Model Serving
|
304
428
|
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
429
|
+
```python
|
430
|
+
# Route requests to appropriate model versions
|
431
|
+
class InferenceService:
|
432
|
+
def predict(self, features: dict, request_version: str) -> BaseModel:
|
433
|
+
# Determine target model version (A/B testing, gradual rollout, etc.)
|
434
|
+
model_version = self.get_model_version(features["user_id"])
|
435
|
+
|
436
|
+
# Migrate request to model's expected format
|
437
|
+
model_input = manager.migrate(
|
438
|
+
features,
|
439
|
+
"PredictionRequest",
|
440
|
+
from_version=request_version,
|
441
|
+
to_version=model_version
|
442
|
+
)
|
443
|
+
|
444
|
+
# Run inference
|
445
|
+
prediction = self.models[model_version].predict(model_input)
|
446
|
+
|
447
|
+
# Normalize output for logging/analytics
|
448
|
+
return manager.migrate(
|
449
|
+
prediction,
|
450
|
+
"PredictionResponse",
|
451
|
+
from_version=model_version,
|
452
|
+
to_version="3.0.0"
|
453
|
+
)
|
309
454
|
```
|
310
455
|
|
456
|
+
See [examples/](examples/) for complete runnable code:
|
457
|
+
- `config_file_migration.py` - CLI/desktop app config file evolution
|
458
|
+
- `message_queue_consumer.py` - Kafka/RabbitMQ/SQS consumer handling multiple
|
459
|
+
schemas
|
460
|
+
- `etl_data_import.py` - CSV/JSON/Excel import pipeline with historical data
|
461
|
+
- `ml_inference_pipeline.py` - ML model serving with feature evolution
|
462
|
+
- `advanced_features.py` - Complex Pydantic features (unions, nested models,
|
463
|
+
validators)
|
464
|
+
|
311
465
|
## Contributing
|
312
466
|
|
313
467
|
For guidance on setting up a development environment and how to make a
|