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.
pyrmute/model_manager.py CHANGED
@@ -1,6 +1,7 @@
1
1
  """Model manager."""
2
2
 
3
- from collections.abc import Callable
3
+ from collections.abc import Callable, Iterable
4
+ from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
4
5
  from pathlib import Path
5
6
  from typing import Any, Self
6
7
 
@@ -9,6 +10,13 @@ from pydantic import BaseModel
9
10
  from ._migration_manager import MigrationManager
10
11
  from ._registry import Registry
11
12
  from ._schema_manager import SchemaManager
13
+ from .exceptions import MigrationError, ModelNotFoundError
14
+ from .migration_testing import (
15
+ MigrationTestCase,
16
+ MigrationTestResult,
17
+ MigrationTestResults,
18
+ )
19
+ from .model_diff import ModelDiff
12
20
  from .model_version import ModelVersion
13
21
  from .types import (
14
22
  DecoratedBaseModel,
@@ -23,17 +31,19 @@ from .types import (
23
31
  class ModelManager:
24
32
  """High-level interface for versioned model management.
25
33
 
26
- Provides a unified API for model registration, migration, and schema
27
- management.
34
+ ModelManager provides a unified API for managing schema evolution across different
35
+ versions of Pydantic models. It handles model registration, automatic migration
36
+ between versions, schema generation, and batch processing operations.
28
37
 
29
38
  Attributes:
30
- registry: Registry instance.
31
- migration_manager: MigrationManager instance.
32
- schema_manager: SchemaManager instance.
39
+ registry: Registry instance managing all registered model versions.
40
+ migration_manager: MigrationManager instance handling migration logic and paths.
41
+ schema_manager: SchemaManager instance for JSON schema generation and export.
33
42
 
34
- Example:
43
+ Basic Usage:
35
44
  >>> manager = ModelManager()
36
45
  >>>
46
+ >>> # Register model versions
37
47
  >>> @manager.model("User", "1.0.0")
38
48
  ... class UserV1(BaseModel):
39
49
  ... name: str
@@ -43,10 +53,41 @@ class ModelManager:
43
53
  ... name: str
44
54
  ... email: str
45
55
  >>>
56
+ >>> # Define migration between versions
46
57
  >>> @manager.migration("User", "1.0.0", "2.0.0")
47
58
  ... def migrate(data: MigrationData) -> MigrationData:
48
59
  ... return {**data, "email": "unknown@example.com"}
49
- """
60
+ >>>
61
+ >>> # Migrate legacy data
62
+ >>> old_data = {"name": "Alice"}
63
+ >>> user = manager.migrate(old_data, "User", "1.0.0", "2.0.0")
64
+ >>> # Result: UserV2(name="Alice", email="unknown@example.com")
65
+
66
+ Advanced Features:
67
+ >>> # Batch migration with parallel processing
68
+ >>> users = manager.migrate_batch(
69
+ ... legacy_users, "User", "1.0.0", "2.0.0",
70
+ ... parallel=True, max_workers=4
71
+ ... )
72
+ >>>
73
+ >>> # Stream large datasets efficiently
74
+ >>> for user in manager.migrate_batch_streaming(large_dataset, "User", "1.0.0", "2.0.0"):
75
+ ... save_to_database(user)
76
+ >>>
77
+ >>> # Compare versions and export schemas
78
+ >>> diff = manager.diff("User", "1.0.0", "2.0.0")
79
+ >>> print(diff.to_markdown())
80
+ >>> manager.dump_schemas("schemas/", separate_definitions=True)
81
+ >>>
82
+ >>> # Test migrations with validation
83
+ >>> results = manager.test_migration(
84
+ ... "User", "1.0.0", "2.0.0",
85
+ ... test_cases=[
86
+ ... ({"name": "Alice"}, {"name": "Alice", "email": "unknown@example.com"})
87
+ ... ]
88
+ ... )
89
+ >>> results.assert_all_passed()
90
+ """ # noqa: E501
50
91
 
51
92
  def __init__(self: Self) -> None:
52
93
  """Initialize the versioned model manager."""
@@ -60,6 +101,7 @@ class ModelManager:
60
101
  version: str | ModelVersion,
61
102
  schema_generator: JsonSchemaGenerator | None = None,
62
103
  enable_ref: bool = False,
104
+ backward_compatible: bool = False,
63
105
  ) -> Callable[[type[DecoratedBaseModel]], type[DecoratedBaseModel]]:
64
106
  """Register a versioned model.
65
107
 
@@ -67,8 +109,11 @@ class ModelManager:
67
109
  name: Name of the model.
68
110
  version: Semantic version.
69
111
  schema_generator: Optional custom schema generator.
70
- enable_ref: If True, this model can be referenced via $ref in
71
- separate schema files. If False, it will always be inlined.
112
+ enable_ref: If True, this model can be referenced via $ref in separate
113
+ schema files. If False, it will always be inlined.
114
+ backward_compatible: If True, this model does not need a migration function
115
+ to migrate to the next version. If a migration function is defined it
116
+ will use it.
72
117
 
73
118
  Returns:
74
119
  Decorator function for model class.
@@ -84,7 +129,9 @@ class ModelManager:
84
129
  ... class CityV1(BaseModel):
85
130
  ... city: City
86
131
  """
87
- return self.registry.register(name, version, schema_generator, enable_ref)
132
+ return self.registry.register(
133
+ name, version, schema_generator, enable_ref, backward_compatible
134
+ )
88
135
 
89
136
  def migration(
90
137
  self: Self,
@@ -120,6 +167,78 @@ class ModelManager:
120
167
  return self.registry.get_latest(name)
121
168
  return self.registry.get_model(name, version)
122
169
 
170
+ def has_migration_path(
171
+ self: Self,
172
+ name: str,
173
+ from_version: str | ModelVersion,
174
+ to_version: str | ModelVersion,
175
+ ) -> bool:
176
+ """Check if a migration path exists between two versions.
177
+
178
+ Args:
179
+ name: Name of the model.
180
+ from_version: Source version.
181
+ to_version: Target version.
182
+
183
+ Returns:
184
+ True if a migration path exists, False otherwise.
185
+
186
+ Example:
187
+ >>> if manager.has_migration_path("User", "1.0.0", "3.0.0"):
188
+ ... users = manager.migrate_batch(old_users, "User", "1.0.0", "3.0.0")
189
+ ... else:
190
+ ... logger.error("Cannot migrate users to v3.0.0")
191
+ """
192
+ from_ver = (
193
+ ModelVersion.parse(from_version)
194
+ if isinstance(from_version, str)
195
+ else from_version
196
+ )
197
+ to_ver = (
198
+ ModelVersion.parse(to_version)
199
+ if isinstance(to_version, str)
200
+ else to_version
201
+ )
202
+ try:
203
+ self.migration_manager.validate_migration_path(name, from_ver, to_ver)
204
+ return True
205
+ except (KeyError, ModelNotFoundError, MigrationError):
206
+ return False
207
+
208
+ def validate_data(
209
+ self: Self,
210
+ data: MigrationData,
211
+ name: str,
212
+ version: str | ModelVersion,
213
+ ) -> bool:
214
+ """Check if data is valid for a specific model version.
215
+
216
+ Validates whether the provided data conforms to the schema of the specified
217
+ model version without raising an exception.
218
+
219
+ Args:
220
+ data: Data dictionary to validate.
221
+ name: Name of the model.
222
+ version: Semantic version to validate against.
223
+
224
+ Returns:
225
+ True if data is valid for the model version, False otherwise.
226
+
227
+ Example:
228
+ >>> data = {"name": "Alice"}
229
+ >>> is_valid = manager.validate_data(data, "User", "1.0.0")
230
+ >>> # Returns: True
231
+ >>>
232
+ >>> is_valid = manager.validate_data(data, "User", "2.0.0")
233
+ >>> # Returns: False, missing required field 'email'
234
+ """
235
+ try:
236
+ model = self.get(name, version)
237
+ model.model_validate(data)
238
+ return True
239
+ except Exception:
240
+ return False
241
+
123
242
  def migrate(
124
243
  self: Self,
125
244
  data: MigrationData,
@@ -130,7 +249,7 @@ class ModelManager:
130
249
  """Migrate data between versions.
131
250
 
132
251
  Args:
133
- data: Data dictionary or BaseModel to migrate.
252
+ data: Data dictionary to migrate.
134
253
  name: Name of the model.
135
254
  from_version: Source version.
136
255
  to_version: Target version.
@@ -138,12 +257,281 @@ class ModelManager:
138
257
  Returns:
139
258
  Migrated BaseModel.
140
259
  """
141
- migrated_data = self.migration_manager.migrate(
142
- data, name, from_version, to_version
143
- )
260
+ migrated_data = self.migrate_data(data, name, from_version, to_version)
144
261
  target_model = self.get(name, to_version)
145
262
  return target_model.model_validate(migrated_data)
146
263
 
264
+ def migrate_data(
265
+ self: Self,
266
+ data: MigrationData,
267
+ name: str,
268
+ from_version: str | ModelVersion,
269
+ to_version: str | ModelVersion,
270
+ ) -> MigrationData:
271
+ """Migrate data between versions.
272
+
273
+ Args:
274
+ data: Data dictionary to migrate.
275
+ name: Name of the model.
276
+ from_version: Source version.
277
+ to_version: Target version.
278
+
279
+ Returns:
280
+ Raw migrated dictionary.
281
+ """
282
+ return self.migration_manager.migrate(data, name, from_version, to_version)
283
+
284
+ def migrate_batch( # noqa: PLR0913
285
+ self: Self,
286
+ data_list: Iterable[MigrationData],
287
+ name: str,
288
+ from_version: str | ModelVersion,
289
+ to_version: str | ModelVersion,
290
+ parallel: bool = False,
291
+ max_workers: int | None = None,
292
+ use_processes: bool = False,
293
+ ) -> list[BaseModel]:
294
+ """Migrate multiple data items between versions.
295
+
296
+ Args:
297
+ data_list: Iterable of data dictionaries to migrate.
298
+ name: Name of the model.
299
+ from_version: Source version.
300
+ to_version: Target version.
301
+ parallel: If True, use parallel processing.
302
+ max_workers: Maximum number of workers for parallel processing. Defaults to
303
+ None (uses executor default).
304
+ use_processes: If True, use ProcessPoolExecutor instead of
305
+ ThreadPoolExecutor. Useful for CPU-intensive migrations.
306
+
307
+ Returns:
308
+ List of migrated BaseModel instances.
309
+
310
+ Example:
311
+ >>> legacy_users = [
312
+ ... {"name": "Alice"},
313
+ ... {"name": "Bob"},
314
+ ... {"name": "Charlie"}
315
+ ... ]
316
+ >>> users = manager.migrate_batch(
317
+ ... legacy_users,
318
+ ... "User",
319
+ ... from_version="1.0.0",
320
+ ... to_version="3.0.0",
321
+ ... parallel=True
322
+ ... )
323
+ """
324
+ data_list = list(data_list)
325
+
326
+ if not data_list:
327
+ return []
328
+
329
+ if not parallel:
330
+ return [
331
+ self.migrate(item, name, from_version, to_version) for item in data_list
332
+ ]
333
+
334
+ executor_class = ProcessPoolExecutor if use_processes else ThreadPoolExecutor
335
+ with executor_class(max_workers=max_workers) as executor:
336
+ futures = [
337
+ executor.submit(self.migrate, item, name, from_version, to_version)
338
+ for item in data_list
339
+ ]
340
+ return [future.result() for future in futures]
341
+
342
+ def migrate_batch_data( # noqa: PLR0913
343
+ self: Self,
344
+ data_list: Iterable[MigrationData],
345
+ name: str,
346
+ from_version: str | ModelVersion,
347
+ to_version: str | ModelVersion,
348
+ parallel: bool = False,
349
+ max_workers: int | None = None,
350
+ use_processes: bool = False,
351
+ ) -> list[MigrationData]:
352
+ """Migrate multiple data items between versions, returning raw dictionaries.
353
+
354
+ Args:
355
+ data_list: Iterable of data dictionaries to migrate.
356
+ name: Name of the model.
357
+ from_version: Source version.
358
+ to_version: Target version.
359
+ parallel: If True, use parallel processing.
360
+ max_workers: Maximum number of workers for parallel processing.
361
+ use_processes: If True, use ProcessPoolExecutor.
362
+
363
+ Returns:
364
+ List of raw migrated dictionaries.
365
+
366
+ Example:
367
+ >>> legacy_data = [{"name": "Alice"}, {"name": "Bob"}]
368
+ >>> migrated_data = manager.migrate_batch_data(
369
+ ... legacy_data,
370
+ ... "User",
371
+ ... from_version="1.0.0",
372
+ ... to_version="2.0.0"
373
+ ... )
374
+ """
375
+ data_list = list(data_list)
376
+
377
+ if not data_list:
378
+ return []
379
+
380
+ if not parallel:
381
+ return [
382
+ self.migrate_data(item, name, from_version, to_version)
383
+ for item in data_list
384
+ ]
385
+
386
+ executor_class = ProcessPoolExecutor if use_processes else ThreadPoolExecutor
387
+ with executor_class(max_workers=max_workers) as executor:
388
+ futures = [
389
+ executor.submit(self.migrate_data, item, name, from_version, to_version)
390
+ for item in data_list
391
+ ]
392
+ return [future.result() for future in futures]
393
+
394
+ def migrate_batch_streaming(
395
+ self: Self,
396
+ data_list: Iterable[MigrationData],
397
+ name: str,
398
+ from_version: str | ModelVersion,
399
+ to_version: str | ModelVersion,
400
+ chunk_size: int = 100,
401
+ ) -> Iterable[BaseModel]:
402
+ """Migrate data in chunks, yielding results as they complete.
403
+
404
+ Useful for large datasets where you want to start processing results before all
405
+ migrations complete.
406
+
407
+ Args:
408
+ data_list: Iterable of data dictionaries to migrate.
409
+ name: Name of the model.
410
+ from_version: Source version.
411
+ to_version: Target version.
412
+ chunk_size: Number of items to process in each chunk.
413
+
414
+ Yields:
415
+ Migrated BaseModel instances.
416
+
417
+ Example:
418
+ >>> legacy_users = load_large_dataset()
419
+ >>> for user in manager.migrate_batch_streaming(
420
+ ... legacy_users,
421
+ ... "User",
422
+ ... from_version="1.0.0",
423
+ ... to_version="3.0.0"
424
+ ... ):
425
+ ... # Process each user as it's migrated
426
+ ... save_to_database(user)
427
+ """
428
+ chunk = []
429
+
430
+ for item in data_list:
431
+ chunk.append(item)
432
+
433
+ if len(chunk) >= chunk_size:
434
+ yield from self.migrate_batch(chunk, name, from_version, to_version)
435
+ chunk = []
436
+
437
+ if chunk:
438
+ yield from self.migrate_batch(chunk, name, from_version, to_version)
439
+
440
+ def migrate_batch_data_streaming(
441
+ self: Self,
442
+ data_list: Iterable[MigrationData],
443
+ name: str,
444
+ from_version: str | ModelVersion,
445
+ to_version: str | ModelVersion,
446
+ chunk_size: int = 100,
447
+ ) -> Iterable[MigrationData]:
448
+ """Migrate data in chunks, yielding raw dictionaries as they complete.
449
+
450
+ Useful for large datasets where you want to start processing results before all
451
+ migrations complete, without the validation overhead.
452
+
453
+ Args:
454
+ data_list: Iterable of data dictionaries to migrate.
455
+ name: Name of the model.
456
+ from_version: Source version.
457
+ to_version: Target version.
458
+ chunk_size: Number of items to process in each chunk.
459
+
460
+ Yields:
461
+ Raw migrated dictionaries.
462
+
463
+ Example:
464
+ >>> legacy_data = load_large_dataset()
465
+ >>> for data in manager.migrate_batch_data_streaming(
466
+ ... legacy_data,
467
+ ... "User",
468
+ ... from_version="1.0.0",
469
+ ... to_version="3.0.0"
470
+ ... ):
471
+ ... # Process raw data as it's migrated
472
+ ... bulk_insert_to_database(data)
473
+ """
474
+ chunk = []
475
+
476
+ for item in data_list:
477
+ chunk.append(item)
478
+
479
+ if len(chunk) >= chunk_size:
480
+ yield from self.migrate_batch_data(
481
+ chunk, name, from_version, to_version
482
+ )
483
+ chunk = []
484
+
485
+ if chunk:
486
+ yield from self.migrate_batch_data(chunk, name, from_version, to_version)
487
+
488
+ def diff(
489
+ self: Self,
490
+ name: str,
491
+ from_version: str | ModelVersion,
492
+ to_version: str | ModelVersion,
493
+ ) -> ModelDiff:
494
+ """Get a detailed diff between two model versions.
495
+
496
+ Compares field names, types, requirements, and default values to provide a
497
+ comprehensive view of what changed between versions.
498
+
499
+ Args:
500
+ name: Name of the model.
501
+ from_version: Source version.
502
+ to_version: Target version.
503
+
504
+ Returns:
505
+ ModelDiff with detailed change information.
506
+
507
+ Example:
508
+ >>> diff = manager.diff("User", "1.0.0", "2.0.0")
509
+ >>> print(diff.to_markdown())
510
+ >>> print(f"Added: {diff.added_fields}")
511
+ >>> print(f"Removed: {diff.removed_fields}")
512
+ """
513
+ from_ver_str = str(
514
+ ModelVersion.parse(from_version)
515
+ if isinstance(from_version, str)
516
+ else from_version
517
+ )
518
+ to_ver_str = str(
519
+ ModelVersion.parse(to_version)
520
+ if isinstance(to_version, str)
521
+ else to_version
522
+ )
523
+
524
+ from_model = self.get(name, from_version)
525
+ to_model = self.get(name, to_version)
526
+
527
+ return ModelDiff.from_models(
528
+ name=name,
529
+ from_model=from_model,
530
+ to_model=to_model,
531
+ from_version=from_ver_str,
532
+ to_version=to_ver_str,
533
+ )
534
+
147
535
  def get_schema(
148
536
  self: Self,
149
537
  name: str,
@@ -193,8 +581,8 @@ class ModelManager:
193
581
  Args:
194
582
  output_dir: Directory path for output.
195
583
  indent: JSON indentation level.
196
- separate_definitions: If True, create separate schema files for
197
- nested models and use $ref to reference them.
584
+ separate_definitions: If True, create separate schema files for nested
585
+ models and use $ref to reference them.
198
586
  ref_template: Template for $ref URLs when separate_definitions=True.
199
587
  Defaults to relative file references if not provided.
200
588
 
@@ -229,8 +617,8 @@ class ModelManager:
229
617
 
230
618
  Args:
231
619
  output_dir: Directory path for output.
232
- ref_template: Template for $ref URLs. Supports {model} and
233
- {version} placeholders. Defaults to relative file refs.
620
+ ref_template: Template for $ref URLs. Supports {model} and {version}
621
+ placeholders. Defaults to relative file refs.
234
622
  indent: JSON indentation level.
235
623
 
236
624
  Example:
@@ -262,3 +650,97 @@ class ModelManager:
262
650
  List of (model_name, version) tuples for nested models.
263
651
  """
264
652
  return self.schema_manager.get_nested_models(name, version)
653
+
654
+ def test_migration(
655
+ self: Self,
656
+ name: str,
657
+ from_version: str | ModelVersion,
658
+ to_version: str | ModelVersion,
659
+ test_cases: list[tuple[MigrationData, MigrationData] | MigrationTestCase],
660
+ ) -> MigrationTestResults:
661
+ """Test a migration with multiple test cases.
662
+
663
+ Executes a migration on multiple test inputs and validates the outputs match
664
+ expected values. Useful for regression testing and validating migration logic.
665
+
666
+ Args:
667
+ name: Name of the model.
668
+ from_version: Source version to migrate from.
669
+ to_version: Target version to migrate to.
670
+ test_cases: List of test cases, either as (source, target) tuples or
671
+ MigrationTestCase objects. If target is None, only verifies the
672
+ migration completes without errors.
673
+
674
+ Returns:
675
+ MigrationTestResults containing individual results for each test case.
676
+
677
+ Example:
678
+ >>> # Using tuples (source, target)
679
+ >>> results = manager.test_migration(
680
+ ... "User", "1.0.0", "2.0.0",
681
+ ... test_cases=[
682
+ ... ({"name": "Alice"}, {"name": "Alice", "email": "alice@example.com"}),
683
+ ... ({"name": "Bob"}, {"name": "Bob", "email": "bob@example.com"})
684
+ ... ]
685
+ ... )
686
+ >>> assert results.all_passed
687
+ >>>
688
+ >>> # Using MigrationTestCase objects
689
+ >>> results = manager.test_migration(
690
+ ... "User", "1.0.0", "2.0.0",
691
+ ... test_cases=[
692
+ ... MigrationTestCase(
693
+ ... source={"name": "Alice"},
694
+ ... target={"name": "Alice", "email": "alice@example.com"},
695
+ ... description="Standard user migration"
696
+ ... )
697
+ ... ]
698
+ ... )
699
+ >>>
700
+ >>> # Use in pytest
701
+ >>> def test_user_migration():
702
+ ... results = manager.test_migration("User", "1.0.0", "2.0.0", test_cases)
703
+ ... results.assert_all_passed() # Raises AssertionError with details if failed
704
+ >>>
705
+ >>> # Inspect failures
706
+ >>> if not results.all_passed:
707
+ ... for failure in results.failures:
708
+ ... print(f"Failed: {failure.test_case.description}")
709
+ ... print(f" Error: {failure.error}")
710
+ """ # noqa: E501
711
+ results = []
712
+
713
+ for test_case_input in test_cases:
714
+ if isinstance(test_case_input, tuple):
715
+ test_case = MigrationTestCase(
716
+ source=test_case_input[0], target=test_case_input[1]
717
+ )
718
+ else:
719
+ test_case = test_case_input
720
+
721
+ try:
722
+ actual = self.migrate_data(
723
+ test_case.source, name, from_version, to_version
724
+ )
725
+
726
+ if test_case.target is not None:
727
+ passed = actual == test_case.target
728
+ error = None if passed else "Output mismatch"
729
+ else:
730
+ # Just verify it doesn't crash
731
+ passed = True
732
+ error = None
733
+
734
+ results.append(
735
+ MigrationTestResult(
736
+ test_case=test_case, actual=actual, passed=passed, error=error
737
+ )
738
+ )
739
+ except Exception as e:
740
+ results.append(
741
+ MigrationTestResult(
742
+ test_case=test_case, actual={}, passed=False, error=str(e)
743
+ )
744
+ )
745
+
746
+ return MigrationTestResults(results)
pyrmute/model_version.py CHANGED
@@ -3,6 +3,8 @@
3
3
  from dataclasses import dataclass
4
4
  from typing import Self
5
5
 
6
+ from .exceptions import InvalidVersionError
7
+
6
8
 
7
9
  @dataclass(frozen=True, order=True)
8
10
  class ModelVersion:
@@ -29,19 +31,25 @@ class ModelVersion:
29
31
  Parsed Version instance.
30
32
 
31
33
  Raises:
32
- ValueError: If version string format is invalid.
34
+ InvalidVersionError: If version string format is invalid.
33
35
  """
34
- parts = version_str.split(".")
35
- if len(parts) != 3: # noqa: PLR2004
36
- raise ValueError(f"Invalid version format: {version_str}")
37
-
38
36
  try:
39
- major, minor, patch = int(parts[0]), int(parts[1]), int(parts[2])
37
+ parts = version_str.split(".")
38
+ if len(parts) != 3: # noqa: PLR2004
39
+ raise InvalidVersionError(
40
+ version_str, "Version must have exactly 3 parts (major.minor.patch)"
41
+ )
42
+
43
+ major, minor, patch = map(int, parts)
40
44
  if major < 0 or minor < 0 or patch < 0:
41
- raise ValueError(f"Invalid version format: {version_str}")
45
+ raise InvalidVersionError(
46
+ version_str, "Version parts must be positive integers"
47
+ )
42
48
  return cls(major, minor, patch)
43
49
  except ValueError as e:
44
- raise ValueError(f"Invalid version format: {version_str}") from e
50
+ raise InvalidVersionError(
51
+ version_str, f"Version parts must be integers: {e}"
52
+ ) from e
45
53
 
46
54
  def __str__(self: Self) -> str:
47
55
  """Return string representation of version.
pyrmute/types.py CHANGED
@@ -1,5 +1,7 @@
1
1
  """Type aliases needed in the package."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  from collections.abc import Callable
4
6
  from typing import Any, TypeAlias, TypeVar
5
7
 
@@ -9,15 +11,18 @@ from .model_version import ModelVersion
9
11
 
10
12
  DecoratedBaseModel = TypeVar("DecoratedBaseModel", bound=BaseModel)
11
13
 
12
- JsonValue: TypeAlias = dict[str, Any] | list[Any] | str | int | float | bool | None
14
+ JsonValue: TypeAlias = (
15
+ int | float | str | bool | None | list["JsonValue"] | dict[str, "JsonValue"]
16
+ )
13
17
  JsonSchema: TypeAlias = dict[str, JsonValue]
14
- JsonSchemaDefinitions: TypeAlias = dict[str, JsonSchema]
18
+ JsonSchemaDefinitions: TypeAlias = dict[str, JsonValue]
15
19
  JsonSchemaGenerator: TypeAlias = Callable[[type[BaseModel]], JsonSchema]
16
20
  SchemaGenerators: TypeAlias = dict[ModelVersion, JsonSchemaGenerator]
17
21
 
18
22
  MigrationData: TypeAlias = dict[str, Any]
19
23
  MigrationFunc: TypeAlias = Callable[[MigrationData], MigrationData]
20
24
 
25
+
21
26
  MigrationKey: TypeAlias = tuple[ModelVersion, ModelVersion]
22
27
  MigrationMap: TypeAlias = dict[MigrationKey, MigrationFunc]
23
28