pyrmute 0.4.0__py3-none-any.whl → 0.5.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/__init__.py +4 -0
- pyrmute/_registry.py +0 -8
- pyrmute/_schema_manager.py +202 -31
- pyrmute/_version.py +2 -2
- pyrmute/model_manager.py +237 -138
- pyrmute/schema_config.py +130 -0
- pyrmute/types.py +3 -2
- {pyrmute-0.4.0.dist-info → pyrmute-0.5.0.dist-info}/METADATA +17 -2
- pyrmute-0.5.0.dist-info/RECORD +18 -0
- pyrmute-0.4.0.dist-info/RECORD +0 -17
- {pyrmute-0.4.0.dist-info → pyrmute-0.5.0.dist-info}/WHEEL +0 -0
- {pyrmute-0.4.0.dist-info → pyrmute-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {pyrmute-0.4.0.dist-info → pyrmute-0.5.0.dist-info}/top_level.txt +0 -0
pyrmute/__init__.py
CHANGED
@@ -19,8 +19,10 @@ from .migration_testing import (
|
|
19
19
|
from .model_diff import ModelDiff
|
20
20
|
from .model_manager import ModelManager
|
21
21
|
from .model_version import ModelVersion
|
22
|
+
from .schema_config import SchemaConfig
|
22
23
|
from .types import (
|
23
24
|
JsonSchema,
|
25
|
+
JsonSchemaMode,
|
24
26
|
MigrationFunc,
|
25
27
|
ModelData,
|
26
28
|
NestedModelInfo,
|
@@ -29,6 +31,7 @@ from .types import (
|
|
29
31
|
__all__ = [
|
30
32
|
"InvalidVersionError",
|
31
33
|
"JsonSchema",
|
34
|
+
"JsonSchemaMode",
|
32
35
|
"MigrationError",
|
33
36
|
"MigrationFunc",
|
34
37
|
"MigrationManager",
|
@@ -43,6 +46,7 @@ __all__ = [
|
|
43
46
|
"ModelVersion",
|
44
47
|
"NestedModelInfo",
|
45
48
|
"Registry",
|
49
|
+
"SchemaConfig",
|
46
50
|
"SchemaManager",
|
47
51
|
"VersionedModelError",
|
48
52
|
"__version__",
|
pyrmute/_registry.py
CHANGED
@@ -10,11 +10,9 @@ from .exceptions import ModelNotFoundError
|
|
10
10
|
from .model_version import ModelVersion
|
11
11
|
from .types import (
|
12
12
|
DecoratedBaseModel,
|
13
|
-
JsonSchemaGenerator,
|
14
13
|
MigrationMap,
|
15
14
|
ModelMetadata,
|
16
15
|
ModelName,
|
17
|
-
SchemaGenerators,
|
18
16
|
VersionedModels,
|
19
17
|
)
|
20
18
|
|
@@ -28,7 +26,6 @@ class Registry:
|
|
28
26
|
Attributes:
|
29
27
|
_models: Dictionary mapping model names to version-model mappings.
|
30
28
|
_migrations: Dictionary storing migration functions between versions.
|
31
|
-
_schema_generators: Dictionary storing custom schema generators.
|
32
29
|
_model_metadata: Dictionary mapping model classes to (name, version).
|
33
30
|
_ref_enabled: Dictionary tracking which models have enable_ref=True.
|
34
31
|
"""
|
@@ -37,7 +34,6 @@ class Registry:
|
|
37
34
|
"""Initialize the model registry."""
|
38
35
|
self._models: dict[ModelName, VersionedModels] = defaultdict(dict)
|
39
36
|
self._migrations: dict[ModelName, MigrationMap] = defaultdict(dict)
|
40
|
-
self._schema_generators: dict[ModelName, SchemaGenerators] = defaultdict(dict)
|
41
37
|
self._model_metadata: dict[type[BaseModel], ModelMetadata] = {}
|
42
38
|
self._ref_enabled: dict[ModelName, set[ModelVersion]] = defaultdict(set)
|
43
39
|
self._backward_compatible_enabled: dict[ModelName, set[ModelVersion]] = (
|
@@ -48,7 +44,6 @@ class Registry:
|
|
48
44
|
self: Self,
|
49
45
|
name: ModelName,
|
50
46
|
version: str | ModelVersion,
|
51
|
-
schema_generator: JsonSchemaGenerator | None = None,
|
52
47
|
enable_ref: bool = False,
|
53
48
|
backward_compatible: bool = False,
|
54
49
|
) -> Callable[[type[DecoratedBaseModel]], type[DecoratedBaseModel]]:
|
@@ -57,7 +52,6 @@ class Registry:
|
|
57
52
|
Args:
|
58
53
|
name: Name of the model.
|
59
54
|
version: Semantic version string or ModelVersion instance.
|
60
|
-
schema_generator: Optional custom schema generator function.
|
61
55
|
enable_ref: If True, this model can be referenced via $ref in separate
|
62
56
|
schema files. If False, it will always be inlined.
|
63
57
|
backward_compatible: If True, this model does not need a migration function
|
@@ -78,8 +72,6 @@ class Registry:
|
|
78
72
|
def decorator(cls: type[DecoratedBaseModel]) -> type[DecoratedBaseModel]:
|
79
73
|
self._models[name][ver] = cls
|
80
74
|
self._model_metadata[cls] = (name, ver)
|
81
|
-
if schema_generator:
|
82
|
-
self._schema_generators[name][ver] = schema_generator
|
83
75
|
if enable_ref:
|
84
76
|
self._ref_enabled[name].add(ver)
|
85
77
|
if backward_compatible:
|
pyrmute/_schema_manager.py
CHANGED
@@ -1,22 +1,27 @@
|
|
1
|
-
"""Schema manager."""
|
1
|
+
"""Schema manager with customizable generation and transformers."""
|
2
2
|
|
3
3
|
import json
|
4
|
+
from collections import defaultdict
|
4
5
|
from pathlib import Path
|
5
|
-
from typing import Any, Self, get_args, get_origin
|
6
|
+
from typing import Any, Self, cast, get_args, get_origin
|
6
7
|
|
7
8
|
from pydantic import BaseModel
|
8
9
|
from pydantic.fields import FieldInfo
|
10
|
+
from pydantic.json_schema import GenerateJsonSchema
|
9
11
|
|
10
12
|
from ._registry import Registry
|
11
13
|
from .exceptions import ModelNotFoundError
|
12
14
|
from .model_version import ModelVersion
|
15
|
+
from .schema_config import SchemaConfig
|
13
16
|
from .types import (
|
14
17
|
JsonSchema,
|
15
18
|
JsonSchemaDefinitions,
|
19
|
+
JsonSchemaGenerator,
|
16
20
|
JsonValue,
|
17
21
|
ModelMetadata,
|
18
22
|
ModelName,
|
19
23
|
NestedModelInfo,
|
24
|
+
SchemaTransformer,
|
20
25
|
)
|
21
26
|
|
22
27
|
|
@@ -24,53 +29,217 @@ class SchemaManager:
|
|
24
29
|
"""Manager for JSON schema generation and export.
|
25
30
|
|
26
31
|
Handles schema generation from Pydantic models with support for custom schema
|
27
|
-
generators and
|
32
|
+
generators, global configuration, per-call overrides, and schema transformers.
|
28
33
|
|
29
34
|
Attributes:
|
30
35
|
registry: Reference to the Registry.
|
36
|
+
default_config: Default schema generation configuration.
|
31
37
|
"""
|
32
38
|
|
33
|
-
def __init__(
|
39
|
+
def __init__(
|
40
|
+
self: Self, registry: Registry, default_config: SchemaConfig | None = None
|
41
|
+
) -> None:
|
34
42
|
"""Initialize the schema manager.
|
35
43
|
|
36
44
|
Args:
|
37
45
|
registry: Registry instance to use.
|
46
|
+
default_config: Default configuration for schema generation.
|
38
47
|
"""
|
39
48
|
self.registry = registry
|
49
|
+
self.default_config = default_config or SchemaConfig()
|
50
|
+
self._transformers: dict[
|
51
|
+
tuple[ModelName, ModelVersion], list[SchemaTransformer]
|
52
|
+
] = defaultdict(list)
|
53
|
+
|
54
|
+
def set_default_schema_generator(
|
55
|
+
self: Self, generator: JsonSchemaGenerator | type[GenerateJsonSchema]
|
56
|
+
) -> None:
|
57
|
+
"""Set the default schema generator for all schemas.
|
58
|
+
|
59
|
+
Args:
|
60
|
+
generator: Custom schema generator - either a callable or GenerateJsonSchema
|
61
|
+
class.
|
62
|
+
|
63
|
+
Example (Callable):
|
64
|
+
>>> def custom_gen(model: type[BaseModel]) -> JsonSchema:
|
65
|
+
... schema = model.model_json_schema()
|
66
|
+
... schema["x-custom"] = True
|
67
|
+
... return schema
|
68
|
+
>>>
|
69
|
+
>>> manager.set_default_schema_generator(custom_gen)
|
70
|
+
|
71
|
+
Example (Class):
|
72
|
+
>>> from pydantic.json_schema import GenerateJsonSchema
|
73
|
+
>>>
|
74
|
+
>>> class CustomGenerator(GenerateJsonSchema):
|
75
|
+
... def generate(
|
76
|
+
... self,
|
77
|
+
... schema: Mapping[str, Any],
|
78
|
+
... mode: JsonSchemaMode = "validation"
|
79
|
+
... ) -> JsonSchema:
|
80
|
+
... json_schema = super().generate(schema, mode=mode)
|
81
|
+
... json_schema["x-custom"] = True
|
82
|
+
... return json_schema
|
83
|
+
>>>
|
84
|
+
>>> manager.set_default_schema_generator(CustomGenerator)
|
85
|
+
"""
|
86
|
+
self.default_config.schema_generator = generator
|
87
|
+
|
88
|
+
def register_transformer(
|
89
|
+
self: Self,
|
90
|
+
name: ModelName,
|
91
|
+
version: str | ModelVersion,
|
92
|
+
transformer: SchemaTransformer,
|
93
|
+
) -> None:
|
94
|
+
"""Register a schema transformer for a specific model version.
|
95
|
+
|
96
|
+
Transformers are applied after schema generation, allowing simple
|
97
|
+
post-processing of schemas without needing to customize the generation process
|
98
|
+
itself.
|
99
|
+
|
100
|
+
Args:
|
101
|
+
name: Name of the model.
|
102
|
+
version: Model version.
|
103
|
+
transformer: Function that takes and returns a JsonSchema.
|
104
|
+
|
105
|
+
Example:
|
106
|
+
>>> def add_examples(schema: JsonSchema) -> JsonSchema:
|
107
|
+
... schema["examples"] = [{"name": "John", "age": 30}]
|
108
|
+
... return schema
|
109
|
+
>>>
|
110
|
+
>>> manager.register_transformer("User", "1.0.0", add_examples)
|
111
|
+
"""
|
112
|
+
ver = ModelVersion.parse(version) if isinstance(version, str) else version
|
113
|
+
key = (name, ver)
|
114
|
+
self._transformers[key].append(transformer)
|
40
115
|
|
41
116
|
def get_schema(
|
42
117
|
self: Self,
|
43
118
|
name: ModelName,
|
44
119
|
version: str | ModelVersion,
|
120
|
+
config: SchemaConfig | None = None,
|
121
|
+
apply_transformers: bool = True,
|
45
122
|
**schema_kwargs: Any,
|
46
123
|
) -> JsonSchema:
|
47
124
|
"""Get JSON schema for a specific model version.
|
48
125
|
|
126
|
+
Execution order:
|
127
|
+
1. Generate base schema using Pydantic
|
128
|
+
2. Apply custom generator (if configured)
|
129
|
+
3. Apply registered transformers (if any)
|
130
|
+
|
49
131
|
Args:
|
50
132
|
name: Name of the model.
|
51
133
|
version: Semantic version.
|
52
|
-
|
134
|
+
config: Optional schema configuration (overrides defaults).
|
135
|
+
apply_transformers: If False, skip transformer application.
|
136
|
+
**schema_kwargs: Additional arguments for schema generation (overrides
|
137
|
+
config).
|
53
138
|
|
54
139
|
Returns:
|
55
140
|
JSON schema dictionary.
|
141
|
+
|
142
|
+
Example:
|
143
|
+
>>> # Use default config
|
144
|
+
>>> schema = manager.get_schema("User", "1.0.0")
|
145
|
+
>>>
|
146
|
+
>>> # Override with custom config
|
147
|
+
>>> config = SchemaConfig(mode="serialization", by_alias=False)
|
148
|
+
>>> schema = manager.get_schema("User", "1.0.0", config=config)
|
149
|
+
>>>
|
150
|
+
>>> # Quick override with kwargs
|
151
|
+
>>> schema = manager.get_schema("User", "1.0.0", mode="serialization")
|
152
|
+
>>>
|
153
|
+
>>> # Get base schema without transformers
|
154
|
+
>>> base_schema = manager.get_schema(
|
155
|
+
... "User", "1.0.0",
|
156
|
+
... apply_transformers=False
|
157
|
+
... )
|
56
158
|
"""
|
57
159
|
ver = ModelVersion.parse(version) if isinstance(version, str) else version
|
58
160
|
model = self.registry.get_model(name, ver)
|
59
161
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
162
|
+
# Always use the config-based approach
|
163
|
+
final_config = self.default_config
|
164
|
+
if config is not None:
|
165
|
+
final_config = final_config.merge_with(config)
|
166
|
+
|
167
|
+
if schema_kwargs:
|
168
|
+
kwargs_config = SchemaConfig(extra_kwargs=schema_kwargs)
|
169
|
+
final_config = final_config.merge_with(kwargs_config)
|
170
|
+
|
171
|
+
schema: JsonSchema
|
172
|
+
if final_config.is_callable_generator():
|
173
|
+
schema = final_config.schema_generator(model) # type: ignore
|
174
|
+
else:
|
175
|
+
schema = model.model_json_schema(**final_config.to_kwargs())
|
176
|
+
|
177
|
+
if apply_transformers:
|
178
|
+
key = (name, ver)
|
179
|
+
if key in self._transformers:
|
180
|
+
for transformer in self._transformers[key]:
|
181
|
+
schema = transformer(schema)
|
182
|
+
|
183
|
+
return schema
|
184
|
+
|
185
|
+
def get_transformers(
|
186
|
+
self: Self,
|
187
|
+
name: ModelName,
|
188
|
+
version: str | ModelVersion,
|
189
|
+
) -> list[SchemaTransformer]:
|
190
|
+
"""Get all transformers registered for a model version.
|
191
|
+
|
192
|
+
Args:
|
193
|
+
name: Name of the model.
|
194
|
+
version: Model version.
|
195
|
+
|
196
|
+
Returns:
|
197
|
+
List of transformer functions.
|
198
|
+
"""
|
199
|
+
ver = ModelVersion.parse(version) if isinstance(version, str) else version
|
200
|
+
key = (name, ver)
|
201
|
+
return self._transformers.get(key, [])
|
202
|
+
|
203
|
+
def clear_transformers(
|
204
|
+
self: Self,
|
205
|
+
name: ModelName | None = None,
|
206
|
+
version: str | ModelVersion | None = None,
|
207
|
+
) -> None:
|
208
|
+
"""Clear registered transformers.
|
66
209
|
|
67
|
-
|
210
|
+
Args:
|
211
|
+
name: Optional model name. If None, clears all transformers.
|
212
|
+
version: Optional version. If None (but name provided), clears all versions
|
213
|
+
of that model.
|
214
|
+
|
215
|
+
Example:
|
216
|
+
>>> # Clear all transformers
|
217
|
+
>>> manager.clear_transformers()
|
218
|
+
>>>
|
219
|
+
>>> # Clear all User transformers
|
220
|
+
>>> manager.clear_transformers("User")
|
221
|
+
>>>
|
222
|
+
>>> # Clear specific version
|
223
|
+
>>> manager.clear_transformers("User", "1.0.0")
|
224
|
+
"""
|
225
|
+
if name is None:
|
226
|
+
self._transformers.clear()
|
227
|
+
elif version is None:
|
228
|
+
keys_to_remove = [key for key in self._transformers if key[0] == name]
|
229
|
+
for key in keys_to_remove:
|
230
|
+
del self._transformers[key]
|
231
|
+
else:
|
232
|
+
ver = ModelVersion.parse(version) if isinstance(version, str) else version
|
233
|
+
key = (name, ver)
|
234
|
+
if key in self._transformers:
|
235
|
+
del self._transformers[key]
|
68
236
|
|
69
237
|
def get_schema_with_separate_defs(
|
70
238
|
self: Self,
|
71
239
|
name: ModelName,
|
72
240
|
version: str | ModelVersion,
|
73
241
|
ref_template: str = "{model}_v{version}.json",
|
242
|
+
config: SchemaConfig | None = None,
|
74
243
|
**schema_kwargs: Any,
|
75
244
|
) -> JsonSchema:
|
76
245
|
"""Get JSON schema with separate definition files for nested models.
|
@@ -83,6 +252,7 @@ class SchemaManager:
|
|
83
252
|
version: Semantic version.
|
84
253
|
ref_template: Template for generating $ref URLs. Supports {model} and
|
85
254
|
{version} placeholders.
|
255
|
+
config: Optional schema configuration.
|
86
256
|
**schema_kwargs: Additional arguments for schema generation.
|
87
257
|
|
88
258
|
Returns:
|
@@ -91,16 +261,16 @@ class SchemaManager:
|
|
91
261
|
Example:
|
92
262
|
>>> schema = manager.get_schema_with_separate_defs(
|
93
263
|
... "User", "2.0.0",
|
94
|
-
... ref_template="https://example.com/schemas/{model}_v{version}.json"
|
264
|
+
... ref_template="https://example.com/schemas/{model}_v{version}.json",
|
265
|
+
... mode="serialization"
|
95
266
|
... )
|
96
267
|
"""
|
97
268
|
ver = ModelVersion.parse(version) if isinstance(version, str) else version
|
98
|
-
schema = self.get_schema(name, ver, **schema_kwargs)
|
269
|
+
schema = self.get_schema(name, ver, config=config, **schema_kwargs)
|
99
270
|
|
100
|
-
# Extract and replace definitions with external references
|
101
271
|
if "$defs" in schema or "definitions" in schema:
|
102
272
|
defs_key = "$defs" if "$defs" in schema else "definitions"
|
103
|
-
definitions
|
273
|
+
definitions = cast("JsonSchemaDefinitions", schema.pop(defs_key, {}))
|
104
274
|
|
105
275
|
# Update all $ref in the schema to point to external files
|
106
276
|
schema = self._replace_refs_with_external(schema, definitions, ref_template)
|
@@ -209,11 +379,14 @@ class SchemaManager:
|
|
209
379
|
return (name, version)
|
210
380
|
return None
|
211
381
|
|
212
|
-
def get_all_schemas(
|
382
|
+
def get_all_schemas(
|
383
|
+
self: Self, name: ModelName, config: SchemaConfig | None = None
|
384
|
+
) -> dict[ModelVersion, JsonSchema]:
|
213
385
|
"""Get all schemas for a model across all versions.
|
214
386
|
|
215
387
|
Args:
|
216
388
|
name: Name of the model.
|
389
|
+
config: Optional schema configuration.
|
217
390
|
|
218
391
|
Returns:
|
219
392
|
Dictionary mapping versions to their schemas.
|
@@ -225,7 +398,7 @@ class SchemaManager:
|
|
225
398
|
raise ModelNotFoundError(name)
|
226
399
|
|
227
400
|
return {
|
228
|
-
version: self.get_schema(name, version)
|
401
|
+
version: self.get_schema(name, version, config=config)
|
229
402
|
for version in self.registry._models[name]
|
230
403
|
}
|
231
404
|
|
@@ -235,6 +408,7 @@ class SchemaManager:
|
|
235
408
|
indent: int = 2,
|
236
409
|
separate_definitions: bool = False,
|
237
410
|
ref_template: str | None = None,
|
411
|
+
config: SchemaConfig | None = None,
|
238
412
|
) -> None:
|
239
413
|
"""Dump all schemas to JSON files.
|
240
414
|
|
@@ -245,27 +419,24 @@ class SchemaManager:
|
|
245
419
|
models that have enable_ref=True.
|
246
420
|
ref_template: Template for $ref URLs when separate_definitions=True.
|
247
421
|
Defaults to relative file references if not provided.
|
422
|
+
config: Optional schema configuration for all exported schemas.
|
248
423
|
|
249
424
|
Example:
|
250
|
-
>>> #
|
251
|
-
>>>
|
252
|
-
|
253
|
-
|
254
|
-
>>> manager.dump_schemas("schemas/", separate_definitions=True)
|
255
|
-
>>>
|
256
|
-
>>> # Separate sub-schemas with absolute URLs
|
257
|
-
>>> manager.dump_schemas(
|
258
|
-
... "schemas/",
|
259
|
-
... separate_definitions=True,
|
260
|
-
... ref_template="https://example.com/schemas/{model}_v{version}.json"
|
425
|
+
>>> # Export with custom schema generator
|
426
|
+
>>> config = SchemaConfig(
|
427
|
+
... schema_generator=CustomGenerator,
|
428
|
+
... mode="serialization"
|
261
429
|
... )
|
430
|
+
>>> manager.dump_schemas("schemas/", config=config)
|
262
431
|
"""
|
263
432
|
output_path = Path(output_dir)
|
264
433
|
output_path.mkdir(parents=True, exist_ok=True)
|
265
434
|
|
266
435
|
if not separate_definitions:
|
267
436
|
for name in self.registry._models:
|
268
|
-
for version, schema in self.get_all_schemas(
|
437
|
+
for version, schema in self.get_all_schemas(
|
438
|
+
name, config=config
|
439
|
+
).items():
|
269
440
|
file_path = output_path / f"{name}_v{version}.json"
|
270
441
|
with open(file_path, "w", encoding="utf-8") as f:
|
271
442
|
json.dump(schema, f, indent=indent)
|
@@ -276,7 +447,7 @@ class SchemaManager:
|
|
276
447
|
for name in self.registry._models:
|
277
448
|
for version in self.registry._models[name]:
|
278
449
|
schema = self.get_schema_with_separate_defs(
|
279
|
-
name, version, ref_template
|
450
|
+
name, version, ref_template, config=config
|
280
451
|
)
|
281
452
|
file_path = output_path / f"{name}_v{version}.json"
|
282
453
|
with open(file_path, "w", encoding="utf-8") as f:
|
pyrmute/_version.py
CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
28
28
|
commit_id: COMMIT_ID
|
29
29
|
__commit_id__: COMMIT_ID
|
30
30
|
|
31
|
-
__version__ = version = '0.
|
32
|
-
__version_tuple__ = version_tuple = (0,
|
31
|
+
__version__ = version = '0.5.0'
|
32
|
+
__version_tuple__ = version_tuple = (0, 5, 0)
|
33
33
|
|
34
34
|
__commit_id__ = commit_id = None
|
pyrmute/model_manager.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
"""
|
1
|
+
"""ModelManager class."""
|
2
2
|
|
3
3
|
from collections.abc import Callable, Iterable
|
4
4
|
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
|
@@ -6,6 +6,7 @@ from pathlib import Path
|
|
6
6
|
from typing import Any, Self
|
7
7
|
|
8
8
|
from pydantic import BaseModel
|
9
|
+
from pydantic.json_schema import GenerateJsonSchema
|
9
10
|
|
10
11
|
from ._migration_manager import MigrationManager
|
11
12
|
from ._registry import Registry
|
@@ -19,6 +20,7 @@ from .migration_testing import (
|
|
19
20
|
)
|
20
21
|
from .model_diff import ModelDiff
|
21
22
|
from .model_version import ModelVersion
|
23
|
+
from .schema_config import SchemaConfig
|
22
24
|
from .types import (
|
23
25
|
DecoratedBaseModel,
|
24
26
|
JsonSchema,
|
@@ -26,20 +28,16 @@ from .types import (
|
|
26
28
|
MigrationFunc,
|
27
29
|
ModelData,
|
28
30
|
NestedModelInfo,
|
31
|
+
SchemaTransformer,
|
29
32
|
)
|
30
33
|
|
31
34
|
|
32
35
|
class ModelManager:
|
33
|
-
"""High-level interface for versioned model management.
|
36
|
+
"""High-level interface for versioned model management and schema generation.
|
34
37
|
|
35
38
|
ModelManager provides a unified API for managing schema evolution across different
|
36
39
|
versions of Pydantic models. It handles model registration, automatic migration
|
37
|
-
between versions, schema generation, and batch processing operations.
|
38
|
-
|
39
|
-
Attributes:
|
40
|
-
registry: Registry instance managing all registered model versions.
|
41
|
-
migration_manager: MigrationManager instance handling migration logic and paths.
|
42
|
-
schema_manager: SchemaManager instance for JSON schema generation and export.
|
40
|
+
between versions, customizable schema generation, and batch processing operations.
|
43
41
|
|
44
42
|
Basic Usage:
|
45
43
|
>>> manager = ModelManager()
|
@@ -64,6 +62,56 @@ class ModelManager:
|
|
64
62
|
>>> user = manager.migrate(old_data, "User", "1.0.0", "2.0.0")
|
65
63
|
>>> # Result: UserV2(name="Alice", email="unknown@example.com")
|
66
64
|
|
65
|
+
Custom Schema Generation:
|
66
|
+
>>> from pydantic.json_schema import GenerateJsonSchema
|
67
|
+
>>>
|
68
|
+
>>> class CustomSchemaGenerator(GenerateJsonSchema):
|
69
|
+
... '''Add custom metadata to all schemas.'''
|
70
|
+
... def generate(
|
71
|
+
... self,
|
72
|
+
... schema: Mapping[str, Any],
|
73
|
+
... mode: JsonSchemaMode = "validation"
|
74
|
+
... ) -> JsonSchema:
|
75
|
+
... json_schema = super().generate(schema, mode=mode)
|
76
|
+
... json_schema["x-company"] = "Acme"
|
77
|
+
... json_schema["$schema"] = self.schema_dialect
|
78
|
+
... return json_schema
|
79
|
+
>>>
|
80
|
+
>>> # Set at manager level (applies to all schemas)
|
81
|
+
>>> manager = ModelManager(
|
82
|
+
... default_schema_config=SchemaConfig(
|
83
|
+
... schema_generator=CustomSchemaGenerator,
|
84
|
+
... mode="validation",
|
85
|
+
... by_alias=True
|
86
|
+
... )
|
87
|
+
... )
|
88
|
+
>>>
|
89
|
+
>>> @manager.model("User", "1.0.0")
|
90
|
+
... class User(BaseModel):
|
91
|
+
... name: str = Field(title="Full Name")
|
92
|
+
... email: str
|
93
|
+
>>>
|
94
|
+
>>> # Get schema with default config
|
95
|
+
>>> schema = manager.get_schema("User", "1.0.0")
|
96
|
+
>>> # Will include x-company: 'Acme'
|
97
|
+
|
98
|
+
Schema Transformers:
|
99
|
+
>>> manager = ModelManager()
|
100
|
+
>>>
|
101
|
+
>>> @manager.model("Product", "1.0.0")
|
102
|
+
... class Product(BaseModel):
|
103
|
+
... name: str
|
104
|
+
... price: float
|
105
|
+
>>>
|
106
|
+
>>> # Add transformer for specific model
|
107
|
+
>>> @manager.schema_transformer("Product", "1.0.0")
|
108
|
+
... def add_examples(schema: JsonSchema) -> JsonSchema:
|
109
|
+
... schema["examples"] = [{"name": "Widget", "price": 9.99}]
|
110
|
+
... return schema
|
111
|
+
>>>
|
112
|
+
>>> schema = manager.get_schema("Product", "1.0.0")
|
113
|
+
>>> # Will include examples
|
114
|
+
|
67
115
|
Advanced Features:
|
68
116
|
>>> # Batch migration with parallel processing
|
69
117
|
>>> users = manager.migrate_batch(
|
@@ -72,7 +120,9 @@ class ModelManager:
|
|
72
120
|
... )
|
73
121
|
>>>
|
74
122
|
>>> # Stream large datasets efficiently
|
75
|
-
>>> for user in manager.migrate_batch_streaming(
|
123
|
+
>>> for user in manager.migrate_batch_streaming(
|
124
|
+
... large_dataset, "User", "1.0.0", "2.0.0"
|
125
|
+
... ):
|
76
126
|
... save_to_database(user)
|
77
127
|
>>>
|
78
128
|
>>> # Compare versions and export schemas
|
@@ -84,23 +134,32 @@ class ModelManager:
|
|
84
134
|
>>> results = manager.test_migration(
|
85
135
|
... "User", "1.0.0", "2.0.0",
|
86
136
|
... test_cases=[
|
87
|
-
... (
|
137
|
+
... (
|
138
|
+
... {"name": "Alice"},
|
139
|
+
... {"name": "Alice", "email": "unknown@example.com"}
|
140
|
+
... )
|
88
141
|
... ]
|
89
142
|
... )
|
90
143
|
>>> results.assert_all_passed()
|
91
|
-
"""
|
144
|
+
"""
|
145
|
+
|
146
|
+
def __init__(self: Self, default_schema_config: SchemaConfig | None = None) -> None:
|
147
|
+
"""Initialize the versioned model manager.
|
92
148
|
|
93
|
-
|
94
|
-
|
149
|
+
Args:
|
150
|
+
default_schema_config: Default configuration for schema generation
|
151
|
+
applied to all schema operations unless overridden.
|
152
|
+
"""
|
95
153
|
self._registry = Registry()
|
96
154
|
self._migration_manager = MigrationManager(self._registry)
|
97
|
-
self._schema_manager = SchemaManager(
|
155
|
+
self._schema_manager = SchemaManager(
|
156
|
+
self._registry, default_config=default_schema_config
|
157
|
+
)
|
98
158
|
|
99
159
|
def model(
|
100
160
|
self: Self,
|
101
161
|
name: str,
|
102
162
|
version: str | ModelVersion,
|
103
|
-
schema_generator: JsonSchemaGenerator | None = None,
|
104
163
|
enable_ref: bool = False,
|
105
164
|
backward_compatible: bool = False,
|
106
165
|
) -> Callable[[type[DecoratedBaseModel]], type[DecoratedBaseModel]]:
|
@@ -109,7 +168,6 @@ class ModelManager:
|
|
109
168
|
Args:
|
110
169
|
name: Name of the model.
|
111
170
|
version: Semantic version.
|
112
|
-
schema_generator: Optional custom schema generator.
|
113
171
|
enable_ref: If True, this model can be referenced via $ref in separate
|
114
172
|
schema files. If False, it will always be inlined.
|
115
173
|
backward_compatible: If True, this model does not need a migration function
|
@@ -130,9 +188,7 @@ class ModelManager:
|
|
130
188
|
... class CityV1(BaseModel):
|
131
189
|
... city: City
|
132
190
|
"""
|
133
|
-
return self._registry.register(
|
134
|
-
name, version, schema_generator, enable_ref, backward_compatible
|
135
|
-
)
|
191
|
+
return self._registry.register(name, version, enable_ref, backward_compatible)
|
136
192
|
|
137
193
|
def migration(
|
138
194
|
self: Self,
|
@@ -309,27 +365,12 @@ class ModelManager:
|
|
309
365
|
from_version: Source version.
|
310
366
|
to_version: Target version.
|
311
367
|
parallel: If True, use parallel processing.
|
312
|
-
max_workers: Maximum number of workers for parallel processing.
|
313
|
-
None (uses executor default).
|
368
|
+
max_workers: Maximum number of workers for parallel processing.
|
314
369
|
use_processes: If True, use ProcessPoolExecutor instead of
|
315
|
-
ThreadPoolExecutor.
|
370
|
+
ThreadPoolExecutor.
|
316
371
|
|
317
372
|
Returns:
|
318
373
|
List of migrated BaseModel instances.
|
319
|
-
|
320
|
-
Example:
|
321
|
-
>>> legacy_users = [
|
322
|
-
... {"name": "Alice"},
|
323
|
-
... {"name": "Bob"},
|
324
|
-
... {"name": "Charlie"}
|
325
|
-
... ]
|
326
|
-
>>> users = manager.migrate_batch(
|
327
|
-
... legacy_users,
|
328
|
-
... "User",
|
329
|
-
... from_version="1.0.0",
|
330
|
-
... to_version="3.0.0",
|
331
|
-
... parallel=True
|
332
|
-
... )
|
333
374
|
"""
|
334
375
|
data_list = list(data_list)
|
335
376
|
|
@@ -372,15 +413,6 @@ class ModelManager:
|
|
372
413
|
|
373
414
|
Returns:
|
374
415
|
List of raw migrated dictionaries.
|
375
|
-
|
376
|
-
Example:
|
377
|
-
>>> legacy_data = [{"name": "Alice"}, {"name": "Bob"}]
|
378
|
-
>>> migrated_data = manager.migrate_batch_data(
|
379
|
-
... legacy_data,
|
380
|
-
... "User",
|
381
|
-
... from_version="1.0.0",
|
382
|
-
... to_version="2.0.0"
|
383
|
-
... )
|
384
416
|
"""
|
385
417
|
data_list = list(data_list)
|
386
418
|
|
@@ -411,9 +443,6 @@ class ModelManager:
|
|
411
443
|
) -> Iterable[BaseModel]:
|
412
444
|
"""Migrate data in chunks, yielding results as they complete.
|
413
445
|
|
414
|
-
Useful for large datasets where you want to start processing results before all
|
415
|
-
migrations complete.
|
416
|
-
|
417
446
|
Args:
|
418
447
|
data_list: Iterable of data dictionaries to migrate.
|
419
448
|
name: Name of the model.
|
@@ -423,17 +452,6 @@ class ModelManager:
|
|
423
452
|
|
424
453
|
Yields:
|
425
454
|
Migrated BaseModel instances.
|
426
|
-
|
427
|
-
Example:
|
428
|
-
>>> legacy_users = load_large_dataset()
|
429
|
-
>>> for user in manager.migrate_batch_streaming(
|
430
|
-
... legacy_users,
|
431
|
-
... "User",
|
432
|
-
... from_version="1.0.0",
|
433
|
-
... to_version="3.0.0"
|
434
|
-
... ):
|
435
|
-
... # Process each user as it's migrated
|
436
|
-
... save_to_database(user)
|
437
455
|
"""
|
438
456
|
chunk = []
|
439
457
|
|
@@ -457,9 +475,6 @@ class ModelManager:
|
|
457
475
|
) -> Iterable[ModelData]:
|
458
476
|
"""Migrate data in chunks, yielding raw dictionaries as they complete.
|
459
477
|
|
460
|
-
Useful for large datasets where you want to start processing results before all
|
461
|
-
migrations complete, without the validation overhead.
|
462
|
-
|
463
478
|
Args:
|
464
479
|
data_list: Iterable of data dictionaries to migrate.
|
465
480
|
name: Name of the model.
|
@@ -469,17 +484,6 @@ class ModelManager:
|
|
469
484
|
|
470
485
|
Yields:
|
471
486
|
Raw migrated dictionaries.
|
472
|
-
|
473
|
-
Example:
|
474
|
-
>>> legacy_data = load_large_dataset()
|
475
|
-
>>> for data in manager.migrate_batch_data_streaming(
|
476
|
-
... legacy_data,
|
477
|
-
... "User",
|
478
|
-
... from_version="1.0.0",
|
479
|
-
... to_version="3.0.0"
|
480
|
-
... ):
|
481
|
-
... # Process raw data as it's migrated
|
482
|
-
... bulk_insert_to_database(data)
|
483
487
|
"""
|
484
488
|
chunk = []
|
485
489
|
|
@@ -503,9 +507,6 @@ class ModelManager:
|
|
503
507
|
) -> ModelDiff:
|
504
508
|
"""Get a detailed diff between two model versions.
|
505
509
|
|
506
|
-
Compares field names, types, requirements, and default values to provide a
|
507
|
-
comprehensive view of what changed between versions.
|
508
|
-
|
509
510
|
Args:
|
510
511
|
name: Name of the model.
|
511
512
|
from_version: Source version.
|
@@ -513,12 +514,6 @@ class ModelManager:
|
|
513
514
|
|
514
515
|
Returns:
|
515
516
|
ModelDiff with detailed change information.
|
516
|
-
|
517
|
-
Example:
|
518
|
-
>>> diff = manager.diff("User", "1.0.0", "2.0.0")
|
519
|
-
>>> print(diff.to_markdown())
|
520
|
-
>>> print(f"Added: {diff.added_fields}")
|
521
|
-
>>> print(f"Removed: {diff.removed_fields}")
|
522
517
|
"""
|
523
518
|
from_ver_str = str(
|
524
519
|
ModelVersion.parse(from_version)
|
@@ -542,10 +537,136 @@ class ModelManager:
|
|
542
537
|
to_version=to_ver_str,
|
543
538
|
)
|
544
539
|
|
540
|
+
def set_default_schema_generator(
|
541
|
+
self: Self, generator: JsonSchemaGenerator | type[GenerateJsonSchema]
|
542
|
+
) -> None:
|
543
|
+
"""Set the default schema generator for all schemas.
|
544
|
+
|
545
|
+
This is a convenience method that updates the default schema configuration.
|
546
|
+
|
547
|
+
Args:
|
548
|
+
generator: Custom schema generator - either a callable or GenerateJsonSchema
|
549
|
+
class.
|
550
|
+
|
551
|
+
Example (Callable):
|
552
|
+
>>> def my_generator(model: type[BaseModel]) -> JsonSchema:
|
553
|
+
... schema = model.model_json_schema()
|
554
|
+
... schema["x-custom"] = True
|
555
|
+
... return schema
|
556
|
+
>>>
|
557
|
+
>>> manager = ModelManager()
|
558
|
+
>>> manager.set_default_schema_generator(my_generator)
|
559
|
+
|
560
|
+
Example (Class - Recommended):
|
561
|
+
>>> from pydantic.json_schema import GenerateJsonSchema
|
562
|
+
>>>
|
563
|
+
>>> class MyGenerator(GenerateJsonSchema):
|
564
|
+
... def generate(
|
565
|
+
... self,
|
566
|
+
... schema: Mapping[str, Any],
|
567
|
+
... mode: JsonSchemaMode = "validation"
|
568
|
+
... ) -> JsonSchema:
|
569
|
+
... json_schema = super().generate(schema, mode=mode)
|
570
|
+
... json_schema["x-custom"] = True
|
571
|
+
... json_schema["$schema"] = self.schema_dialect
|
572
|
+
... return json_schema
|
573
|
+
>>>
|
574
|
+
>>> manager = ModelManager()
|
575
|
+
>>> manager.set_default_schema_generator(MyGenerator)
|
576
|
+
>>>
|
577
|
+
>>> # All subsequent schema calls will use MyGenerator
|
578
|
+
>>> schema = manager.get_schema("User", "1.0.0")
|
579
|
+
"""
|
580
|
+
self._schema_manager.set_default_schema_generator(generator)
|
581
|
+
|
582
|
+
def schema_transformer(
|
583
|
+
self: Self,
|
584
|
+
name: str,
|
585
|
+
version: str | ModelVersion,
|
586
|
+
) -> Callable[[SchemaTransformer], SchemaTransformer]:
|
587
|
+
"""Decorator to register a schema transformer for a model version.
|
588
|
+
|
589
|
+
Transformers are simple functions that modify a schema after generation.
|
590
|
+
They're useful for model-specific customizations that don't require deep
|
591
|
+
integration with Pydantic's generation process.
|
592
|
+
|
593
|
+
Args:
|
594
|
+
name: Name of the model.
|
595
|
+
version: Model version.
|
596
|
+
|
597
|
+
Returns:
|
598
|
+
Decorator function.
|
599
|
+
|
600
|
+
Example:
|
601
|
+
>>> @manager.schema_transformer("User", "1.0.0")
|
602
|
+
... def add_auth_metadata(schema: JsonSchema) -> JsonSchema:
|
603
|
+
... schema["x-requires-auth"] = True
|
604
|
+
... schema["x-auth-level"] = 'admin'
|
605
|
+
... return schema
|
606
|
+
>>>
|
607
|
+
>>> @manager.schema_transformer("Product", "2.0.0")
|
608
|
+
... def add_product_examples(schema: JsonSchema) -> JsonSchema:
|
609
|
+
... schema["examples"] = [
|
610
|
+
... {"name": "Widget", "price": 9.99},
|
611
|
+
... {"name": "Gadget", "price": 19.99}
|
612
|
+
... ]
|
613
|
+
... return schema
|
614
|
+
"""
|
615
|
+
|
616
|
+
def decorator(func: SchemaTransformer) -> SchemaTransformer:
|
617
|
+
self._schema_manager.register_transformer(name, version, func)
|
618
|
+
return func
|
619
|
+
|
620
|
+
return decorator
|
621
|
+
|
622
|
+
def get_schema_transformers(
|
623
|
+
self: Self,
|
624
|
+
name: str,
|
625
|
+
version: str | ModelVersion,
|
626
|
+
) -> list[SchemaTransformer]:
|
627
|
+
"""Get all transformers for a model version.
|
628
|
+
|
629
|
+
Args:
|
630
|
+
name: Name of the model.
|
631
|
+
version: Model version.
|
632
|
+
|
633
|
+
Returns:
|
634
|
+
List of transformer functions.
|
635
|
+
|
636
|
+
Example:
|
637
|
+
>>> transformers = manager.get_schema_transformers("User", "1.0.0")
|
638
|
+
>>> print(f"Found {len(transformers)} transformers")
|
639
|
+
"""
|
640
|
+
return self._schema_manager.get_transformers(name, version)
|
641
|
+
|
642
|
+
def clear_schema_transformers(
|
643
|
+
self: Self,
|
644
|
+
name: str | None = None,
|
645
|
+
version: str | ModelVersion | None = None,
|
646
|
+
) -> None:
|
647
|
+
"""Clear schema transformers.
|
648
|
+
|
649
|
+
Args:
|
650
|
+
name: Optional model name. If None, clears all.
|
651
|
+
version: Optional version. If None, clears all versions of model.
|
652
|
+
|
653
|
+
Example:
|
654
|
+
>>> # Clear all transformers
|
655
|
+
>>> manager.clear_schema_transformers()
|
656
|
+
>>>
|
657
|
+
>>> # Clear User transformers
|
658
|
+
>>> manager.clear_schema_transformers("User")
|
659
|
+
>>>
|
660
|
+
>>> # Clear specific version
|
661
|
+
>>> manager.clear_schema_transformers("User", "1.0.0")
|
662
|
+
"""
|
663
|
+
self._schema_manager.clear_transformers(name, version)
|
664
|
+
|
545
665
|
def get_schema(
|
546
666
|
self: Self,
|
547
667
|
name: str,
|
548
668
|
version: str | ModelVersion,
|
669
|
+
config: SchemaConfig | None = None,
|
549
670
|
**kwargs: Any,
|
550
671
|
) -> JsonSchema:
|
551
672
|
"""Get JSON schema for a specific version.
|
@@ -553,12 +674,25 @@ class ModelManager:
|
|
553
674
|
Args:
|
554
675
|
name: Name of the model.
|
555
676
|
version: Semantic version.
|
556
|
-
|
677
|
+
config: Optional schema configuration (overrides default).
|
678
|
+
**kwargs: Additional schema generation arguments (e.g.,
|
679
|
+
mode="serialization").
|
557
680
|
|
558
681
|
Returns:
|
559
682
|
JSON schema dictionary.
|
683
|
+
|
684
|
+
Example:
|
685
|
+
>>> # Use default config
|
686
|
+
>>> schema = manager.get_schema("User", "1.0.0")
|
687
|
+
>>>
|
688
|
+
>>> # Override with custom config
|
689
|
+
>>> config = SchemaConfig(mode="serialization")
|
690
|
+
>>> schema = manager.get_schema("User", "1.0.0", config=config)
|
691
|
+
>>>
|
692
|
+
>>> # Quick override with kwargs
|
693
|
+
>>> schema = manager.get_schema("User", "1.0.0", mode="serialization")
|
560
694
|
"""
|
561
|
-
return self._schema_manager.get_schema(name, version, **kwargs)
|
695
|
+
return self._schema_manager.get_schema(name, version, config=config, **kwargs)
|
562
696
|
|
563
697
|
def list_models(self: Self) -> list[str]:
|
564
698
|
"""Get list of all registered models.
|
@@ -585,6 +719,7 @@ class ModelManager:
|
|
585
719
|
indent: int = 2,
|
586
720
|
separate_definitions: bool = False,
|
587
721
|
ref_template: str | None = None,
|
722
|
+
config: SchemaConfig | None = None,
|
588
723
|
) -> None:
|
589
724
|
"""Export all schemas to JSON files.
|
590
725
|
|
@@ -592,27 +727,30 @@ class ModelManager:
|
|
592
727
|
output_dir: Directory path for output.
|
593
728
|
indent: JSON indentation level.
|
594
729
|
separate_definitions: If True, create separate schema files for nested
|
595
|
-
models and use $ref to reference them.
|
596
|
-
'enable_ref=True'.
|
730
|
+
models and use $ref to reference them.
|
597
731
|
ref_template: Template for $ref URLs when separate_definitions=True.
|
598
|
-
|
732
|
+
config: Optional schema configuration for all exported schemas.
|
599
733
|
|
600
734
|
Example:
|
601
|
-
>>> #
|
602
|
-
>>>
|
603
|
-
|
604
|
-
|
605
|
-
|
735
|
+
>>> # Export with custom generator
|
736
|
+
>>> config = SchemaConfig(
|
737
|
+
... schema_generator=CustomGenerator,
|
738
|
+
... mode="validation"
|
739
|
+
... )
|
740
|
+
>>> manager.dump_schemas("schemas/", config=config)
|
606
741
|
>>>
|
607
|
-
>>> #
|
742
|
+
>>> # Export validation and serialization schemas separately
|
608
743
|
>>> manager.dump_schemas(
|
609
|
-
... "schemas/",
|
610
|
-
...
|
611
|
-
...
|
744
|
+
... "schemas/validation/",
|
745
|
+
... config=SchemaConfig(mode="validation")
|
746
|
+
... )
|
747
|
+
>>> manager.dump_schemas(
|
748
|
+
... "schemas/serialization/",
|
749
|
+
... config=SchemaConfig(mode="serialization")
|
612
750
|
... )
|
613
751
|
"""
|
614
752
|
self._schema_manager.dump_schemas(
|
615
|
-
output_dir, indent, separate_definitions, ref_template
|
753
|
+
output_dir, indent, separate_definitions, ref_template, config=config
|
616
754
|
)
|
617
755
|
|
618
756
|
def get_nested_models(
|
@@ -640,54 +778,15 @@ class ModelManager:
|
|
640
778
|
) -> MigrationTestResults:
|
641
779
|
"""Test a migration with multiple test cases.
|
642
780
|
|
643
|
-
Executes a migration on multiple test inputs and validates the outputs match
|
644
|
-
expected values. Useful for regression testing and validating migration logic.
|
645
|
-
|
646
781
|
Args:
|
647
782
|
name: Name of the model.
|
648
783
|
from_version: Source version to migrate from.
|
649
784
|
to_version: Target version to migrate to.
|
650
|
-
test_cases: List of test cases
|
651
|
-
MigrationTestCase objects. If target is None, only verifies the
|
652
|
-
migration completes without errors.
|
785
|
+
test_cases: List of test cases.
|
653
786
|
|
654
787
|
Returns:
|
655
788
|
MigrationTestResults containing individual results for each test case.
|
656
|
-
|
657
|
-
Example:
|
658
|
-
>>> # Using tuples (source, target)
|
659
|
-
>>> results = manager.test_migration(
|
660
|
-
... "User", "1.0.0", "2.0.0",
|
661
|
-
... test_cases=[
|
662
|
-
... ({"name": "Alice"}, {"name": "Alice", "email": "alice@example.com"}),
|
663
|
-
... ({"name": "Bob"}, {"name": "Bob", "email": "bob@example.com"})
|
664
|
-
... ]
|
665
|
-
... )
|
666
|
-
>>> assert results.all_passed
|
667
|
-
>>>
|
668
|
-
>>> # Using MigrationTestCase objects
|
669
|
-
>>> results = manager.test_migration(
|
670
|
-
... "User", "1.0.0", "2.0.0",
|
671
|
-
... test_cases=[
|
672
|
-
... MigrationTestCase(
|
673
|
-
... source={"name": "Alice"},
|
674
|
-
... target={"name": "Alice", "email": "alice@example.com"},
|
675
|
-
... description="Standard user migration"
|
676
|
-
... )
|
677
|
-
... ]
|
678
|
-
... )
|
679
|
-
>>>
|
680
|
-
>>> # Use in pytest
|
681
|
-
>>> def test_user_migration():
|
682
|
-
... results = manager.test_migration("User", "1.0.0", "2.0.0", test_cases)
|
683
|
-
... results.assert_all_passed() # Raises AssertionError with details if failed
|
684
|
-
>>>
|
685
|
-
>>> # Inspect failures
|
686
|
-
>>> if not results.all_passed:
|
687
|
-
... for failure in results.failures:
|
688
|
-
... print(f"Failed: {failure.test_case.description}")
|
689
|
-
... print(f" Error: {failure.error}")
|
690
|
-
""" # noqa: E501
|
789
|
+
"""
|
691
790
|
results = []
|
692
791
|
|
693
792
|
for test_case_input in test_cases:
|
pyrmute/schema_config.py
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
"""Schema configuration for customized schema generation."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from dataclasses import dataclass, field
|
6
|
+
from typing import TYPE_CHECKING, Any, Self
|
7
|
+
|
8
|
+
from pydantic.json_schema import GenerateJsonSchema
|
9
|
+
|
10
|
+
if TYPE_CHECKING:
|
11
|
+
from .types import JsonSchemaGenerator, JsonSchemaMode
|
12
|
+
|
13
|
+
|
14
|
+
@dataclass
|
15
|
+
class SchemaConfig:
|
16
|
+
"""Configuration for JSON schema generation.
|
17
|
+
|
18
|
+
This class provides fine-grained control over how Pydantic generates JSON schemas,
|
19
|
+
supporting both callable generators and Pydantic's GenerateJsonSchema classes.
|
20
|
+
|
21
|
+
Attributes:
|
22
|
+
schema_generator: Custom schema generator. Can be either:
|
23
|
+
- A callable taking (type[BaseModel]) -> JsonSchema
|
24
|
+
- A subclass of pydantic.json_schema.GenerateJsonSchema
|
25
|
+
mode: Schema generation mode - 'validation' for input validation or
|
26
|
+
'serialization' for output serialization.
|
27
|
+
by_alias: Whether to use field aliases in the schema.
|
28
|
+
ref_template: Template for JSON schema $ref URIs.
|
29
|
+
extra_kwargs: Additional arguments to pass to model_json_schema().
|
30
|
+
|
31
|
+
Example (Callable Generator):
|
32
|
+
>>> def custom_generator(model: type[BaseModel]) -> JsonSchema:
|
33
|
+
... schema = model.model_json_schema()
|
34
|
+
... schema["x-custom"] = "metadata"
|
35
|
+
... return schema
|
36
|
+
>>>
|
37
|
+
>>> config = SchemaConfig(
|
38
|
+
... schema_generator=custom_generator,
|
39
|
+
... mode="validation"
|
40
|
+
... )
|
41
|
+
|
42
|
+
Example (GenerateJsonSchema Class):
|
43
|
+
>>> from pydantic.json_schema import GenerateJsonSchema
|
44
|
+
>>>
|
45
|
+
>>> class CustomSchemaGenerator(GenerateJsonSchema):
|
46
|
+
... def generate(
|
47
|
+
... self,
|
48
|
+
... schema: Mapping[str, Any],
|
49
|
+
... mode: JsonSchemaMode = "validation"
|
50
|
+
... ) -> JsonSchema:
|
51
|
+
... json_schema = super().generate(schema, mode=mode)
|
52
|
+
... json_schema["x-custom"] = "metadata"
|
53
|
+
... return json_schema
|
54
|
+
>>>
|
55
|
+
>>> config = SchemaConfig(
|
56
|
+
... schema_generator=CustomSchemaGenerator,
|
57
|
+
... mode="validation",
|
58
|
+
... by_alias=True
|
59
|
+
... )
|
60
|
+
"""
|
61
|
+
|
62
|
+
schema_generator: JsonSchemaGenerator | type[GenerateJsonSchema] | None = None
|
63
|
+
mode: JsonSchemaMode = "validation"
|
64
|
+
by_alias: bool = True
|
65
|
+
ref_template: str = "#/$defs/{model}"
|
66
|
+
extra_kwargs: dict[str, Any] = field(default_factory=dict)
|
67
|
+
|
68
|
+
def merge_with(self: Self, other: SchemaConfig | None) -> SchemaConfig:
|
69
|
+
"""Merge this config with another, with other taking precedence.
|
70
|
+
|
71
|
+
Args:
|
72
|
+
other: Configuration to merge with (overrides this config).
|
73
|
+
|
74
|
+
Returns:
|
75
|
+
New SchemaConfig with merged values.
|
76
|
+
"""
|
77
|
+
if other is None:
|
78
|
+
return self
|
79
|
+
|
80
|
+
return SchemaConfig(
|
81
|
+
schema_generator=other.schema_generator or self.schema_generator,
|
82
|
+
mode=other.mode if other.mode != "validation" else self.mode,
|
83
|
+
by_alias=other.by_alias if not other.by_alias else self.by_alias,
|
84
|
+
ref_template=other.ref_template
|
85
|
+
if other.ref_template != "#/$defs/{model}"
|
86
|
+
else self.ref_template,
|
87
|
+
extra_kwargs={**self.extra_kwargs, **other.extra_kwargs},
|
88
|
+
)
|
89
|
+
|
90
|
+
def to_kwargs(self: Self) -> dict[str, Any]:
|
91
|
+
"""Convert config to kwargs for model_json_schema().
|
92
|
+
|
93
|
+
Note: If schema_generator is a callable (JsonSchemaGenerator type), it cannot be
|
94
|
+
passed to model_json_schema() and must be handled separately by calling it
|
95
|
+
directly.
|
96
|
+
|
97
|
+
Returns:
|
98
|
+
Dictionary of arguments for Pydantic's model_json_schema(). If
|
99
|
+
schema_generator is a callable, it will NOT be included.
|
100
|
+
"""
|
101
|
+
kwargs = {
|
102
|
+
"mode": self.mode,
|
103
|
+
"by_alias": self.by_alias,
|
104
|
+
"ref_template": self.ref_template,
|
105
|
+
**self.extra_kwargs,
|
106
|
+
}
|
107
|
+
|
108
|
+
# Only add schema_generator if it's a GenerateJsonSchema class
|
109
|
+
# Callable generators are handled separately
|
110
|
+
if (
|
111
|
+
self.schema_generator is not None
|
112
|
+
and isinstance(self.schema_generator, type)
|
113
|
+
and issubclass(self.schema_generator, GenerateJsonSchema)
|
114
|
+
):
|
115
|
+
kwargs["schema_generator"] = self.schema_generator
|
116
|
+
|
117
|
+
return kwargs
|
118
|
+
|
119
|
+
def is_callable_generator(self: Self) -> bool:
|
120
|
+
"""Check if schema_generator is a callable function.
|
121
|
+
|
122
|
+
Returns:
|
123
|
+
True if schema_generator is a callable (not a class).
|
124
|
+
"""
|
125
|
+
if self.schema_generator is None:
|
126
|
+
return False
|
127
|
+
|
128
|
+
return callable(self.schema_generator) and not isinstance(
|
129
|
+
self.schema_generator, type
|
130
|
+
)
|
pyrmute/types.py
CHANGED
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
4
4
|
|
5
5
|
from collections.abc import Callable
|
6
6
|
from dataclasses import dataclass
|
7
|
-
from typing import Any, TypeAlias, TypeVar
|
7
|
+
from typing import Any, Literal, TypeAlias, TypeVar
|
8
8
|
|
9
9
|
from pydantic import BaseModel
|
10
10
|
|
@@ -16,9 +16,10 @@ JsonValue: TypeAlias = (
|
|
16
16
|
int | float | str | bool | None | list["JsonValue"] | dict[str, "JsonValue"]
|
17
17
|
)
|
18
18
|
JsonSchema: TypeAlias = dict[str, JsonValue]
|
19
|
+
JsonSchemaMode = Literal["validation", "serialization"]
|
19
20
|
JsonSchemaDefinitions: TypeAlias = dict[str, JsonValue]
|
20
21
|
JsonSchemaGenerator: TypeAlias = Callable[[type[BaseModel]], JsonSchema]
|
21
|
-
|
22
|
+
SchemaTransformer = Callable[[JsonSchema], JsonSchema]
|
22
23
|
|
23
24
|
ModelData: TypeAlias = dict[str, Any]
|
24
25
|
MigrationFunc: TypeAlias = Callable[[ModelData], ModelData]
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: pyrmute
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.5.0
|
4
4
|
Summary: Pydantic model migrations and schemas
|
5
5
|
Author-email: Matt Ferrera <mattferrera@gmail.com>
|
6
6
|
License: MIT
|
@@ -58,7 +58,7 @@ through multiple versions.
|
|
58
58
|
|
59
59
|
## When to Use pyrmute
|
60
60
|
|
61
|
-
pyrmute
|
61
|
+
pyrmute is useful for handling schema evolution in production systems:
|
62
62
|
|
63
63
|
- **Configuration files** - Upgrade user config files as your CLI/desktop app
|
64
64
|
evolves (`.apprc`, `config.json`, `settings.yaml`)
|
@@ -78,6 +78,21 @@ pyrmute excels at handling schema evolution in production systems:
|
|
78
78
|
See the [examples/](examples/) directory for complete, runnable code
|
79
79
|
demonstrating these patterns.
|
80
80
|
|
81
|
+
## When Not to Use
|
82
|
+
|
83
|
+
pyrmute may not be the right choice if you have:
|
84
|
+
|
85
|
+
- **High-throughput systems** - Runtime migration adds latency to hot paths.
|
86
|
+
Use upfront batch migrations instead.
|
87
|
+
- **Multi-language services** - Python-only. Use Protobuf, Avro, or JSON
|
88
|
+
Schema for polyglot architectures.
|
89
|
+
- **Existing schema registries** - Already using Confluent/AWS Glue? Stick
|
90
|
+
with them for compatibility enforcement and governance.
|
91
|
+
- **Stable schemas** - Models rarely change? Traditional migration tools are
|
92
|
+
simpler and more maintainable.
|
93
|
+
- **Database DDL changes** - pyrmute transforms data, not database schemas.
|
94
|
+
Alembic/Flyway or other ORMs may still be needed to alter tables.
|
95
|
+
|
81
96
|
## Help
|
82
97
|
|
83
98
|
See [documentation](https://mferrera.github.io/pyrmute/) for complete guides
|
@@ -0,0 +1,18 @@
|
|
1
|
+
pyrmute/__init__.py,sha256=vgq5e3jmr2FHmSnnnf-Kvl4Y6oGra3BDl5p_CaHDYLQ,1234
|
2
|
+
pyrmute/_migration_manager.py,sha256=TFws66RsEdKLpvjDDQB1pKgeeyUe5WoutacTaeDsZoE,26154
|
3
|
+
pyrmute/_registry.py,sha256=eEhMagcpUeZSPNZlIAuTzXcCkPGHTxfaIfEYEoEiFU8,5680
|
4
|
+
pyrmute/_schema_manager.py,sha256=9NmyooG92gcOMOYNLRuC8isGUvivIf_Hp8kdVC8lmu8,18622
|
5
|
+
pyrmute/_version.py,sha256=fvHpBU3KZKRinkriKdtAt3crenOyysELF-M9y3ozg3U,704
|
6
|
+
pyrmute/exceptions.py,sha256=Q57cUuzzMdkIl5Q0_VyLobpdB0WcrE0ggfC-LBoX2Uo,1681
|
7
|
+
pyrmute/migration_testing.py,sha256=fpKT2u7pgPRpswb4PUvbd-fQ3W76svNWvEVYVDmb3Dg,5066
|
8
|
+
pyrmute/model_diff.py,sha256=vMa2NTYFqt9E7UYDZH4PQmLcoxQw5Sj-nPpUHB_53Ig,9594
|
9
|
+
pyrmute/model_manager.py,sha256=EC3xN4Jve77Fk3xJjC3KrU_sY3Eoae-ucJE8wQ3bO3M,27778
|
10
|
+
pyrmute/model_version.py,sha256=ftNDuJlN3S5ZKQK8DKqqwfBDRiz4rGCYn-aJ3n6Zmqk,2025
|
11
|
+
pyrmute/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
12
|
+
pyrmute/schema_config.py,sha256=ttM4a1-EUUlxmsol6L4rN_Mse-77Y-yidn0ezojy0uY,4767
|
13
|
+
pyrmute/types.py,sha256=rzRxOYfh4WPVR1KoNT3vC3UjuBlTarMnNL6Z1Y5icrw,1237
|
14
|
+
pyrmute-0.5.0.dist-info/licenses/LICENSE,sha256=otWInySiZeGwhHqQQ7n7nxM5QBSBe2CzeGEmQDZEz8Q,1119
|
15
|
+
pyrmute-0.5.0.dist-info/METADATA,sha256=QOv8NMAZ_sw-k2bFOHXM79XGnPUBArt4PRELq5cUdMY,15169
|
16
|
+
pyrmute-0.5.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
17
|
+
pyrmute-0.5.0.dist-info/top_level.txt,sha256=C8QtzqE6yBHkeewSp1QewvsyeHj_VQLYjSa5HLtMiow,8
|
18
|
+
pyrmute-0.5.0.dist-info/RECORD,,
|
pyrmute-0.4.0.dist-info/RECORD
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
pyrmute/__init__.py,sha256=j2pbMYswL0xR8FZwZDg-qAw6HwFpA3KPhY06_2yc_U0,1132
|
2
|
-
pyrmute/_migration_manager.py,sha256=TFws66RsEdKLpvjDDQB1pKgeeyUe5WoutacTaeDsZoE,26154
|
3
|
-
pyrmute/_registry.py,sha256=iUjMPd6CYgyvWT8PxZqHWBZnsHrX25fOPDi_-k_QDJs,6124
|
4
|
-
pyrmute/_schema_manager.py,sha256=eun8PTL9Gv1XAMVKmE3tYmjdrcf701-IapUXjb6WDL0,12122
|
5
|
-
pyrmute/_version.py,sha256=2_0GUP7yBCXRus-qiJKxQD62z172WSs1sQ6DVpPsbmM,704
|
6
|
-
pyrmute/exceptions.py,sha256=Q57cUuzzMdkIl5Q0_VyLobpdB0WcrE0ggfC-LBoX2Uo,1681
|
7
|
-
pyrmute/migration_testing.py,sha256=fpKT2u7pgPRpswb4PUvbd-fQ3W76svNWvEVYVDmb3Dg,5066
|
8
|
-
pyrmute/model_diff.py,sha256=vMa2NTYFqt9E7UYDZH4PQmLcoxQw5Sj-nPpUHB_53Ig,9594
|
9
|
-
pyrmute/model_manager.py,sha256=a6ecd-lZ3iliP3lqgCAi7xLeFlBh50kBA-m6gLGKRx4,24585
|
10
|
-
pyrmute/model_version.py,sha256=ftNDuJlN3S5ZKQK8DKqqwfBDRiz4rGCYn-aJ3n6Zmqk,2025
|
11
|
-
pyrmute/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
12
|
-
pyrmute/types.py,sha256=56IH8Rl9AmVh_w3V6PbSSEwaPrBSfc4pYrtcxodvlT0,1187
|
13
|
-
pyrmute-0.4.0.dist-info/licenses/LICENSE,sha256=otWInySiZeGwhHqQQ7n7nxM5QBSBe2CzeGEmQDZEz8Q,1119
|
14
|
-
pyrmute-0.4.0.dist-info/METADATA,sha256=-KbIQN_INu7ZGfyEO65qmSGsWAXiTHQXR_7F5f-enOQ,14480
|
15
|
-
pyrmute-0.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
16
|
-
pyrmute-0.4.0.dist-info/top_level.txt,sha256=C8QtzqE6yBHkeewSp1QewvsyeHj_VQLYjSa5HLtMiow,8
|
17
|
-
pyrmute-0.4.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|