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/__init__.py +28 -1
- pyrmute/_migration_manager.py +105 -30
- pyrmute/_registry.py +24 -11
- pyrmute/_schema_manager.py +14 -27
- pyrmute/_version.py +2 -2
- pyrmute/exceptions.py +55 -0
- pyrmute/migration_testing.py +161 -0
- pyrmute/model_diff.py +272 -0
- pyrmute/model_manager.py +501 -19
- pyrmute/model_version.py +16 -8
- pyrmute/types.py +7 -2
- pyrmute-0.2.0.dist-info/METADATA +347 -0
- pyrmute-0.2.0.dist-info/RECORD +17 -0
- pyrmute-0.1.0.dist-info/METADATA +0 -130
- pyrmute-0.1.0.dist-info/RECORD +0 -14
- {pyrmute-0.1.0.dist-info → pyrmute-0.2.0.dist-info}/WHEEL +0 -0
- {pyrmute-0.1.0.dist-info → pyrmute-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {pyrmute-0.1.0.dist-info → pyrmute-0.2.0.dist-info}/top_level.txt +0 -0
pyrmute/__init__.py
CHANGED
@@ -5,16 +5,43 @@ and schema management.
|
|
5
5
|
"""
|
6
6
|
|
7
7
|
from ._version import __version__
|
8
|
+
from .exceptions import (
|
9
|
+
InvalidVersionError,
|
10
|
+
MigrationError,
|
11
|
+
ModelNotFoundError,
|
12
|
+
VersionedModelError,
|
13
|
+
)
|
14
|
+
from .migration_testing import (
|
15
|
+
MigrationTestCase,
|
16
|
+
MigrationTestResult,
|
17
|
+
MigrationTestResults,
|
18
|
+
)
|
19
|
+
from .model_diff import ModelDiff
|
8
20
|
from .model_manager import ModelManager
|
9
21
|
from .model_version import ModelVersion
|
10
|
-
from .types import
|
22
|
+
from .types import (
|
23
|
+
JsonSchema,
|
24
|
+
JsonValue,
|
25
|
+
MigrationData,
|
26
|
+
MigrationFunc,
|
27
|
+
ModelMetadata,
|
28
|
+
)
|
11
29
|
|
12
30
|
__all__ = [
|
31
|
+
"InvalidVersionError",
|
13
32
|
"JsonSchema",
|
33
|
+
"JsonValue",
|
14
34
|
"MigrationData",
|
35
|
+
"MigrationError",
|
15
36
|
"MigrationFunc",
|
37
|
+
"MigrationTestCase",
|
38
|
+
"MigrationTestResult",
|
39
|
+
"MigrationTestResults",
|
40
|
+
"ModelDiff",
|
16
41
|
"ModelManager",
|
17
42
|
"ModelMetadata",
|
43
|
+
"ModelNotFoundError",
|
18
44
|
"ModelVersion",
|
45
|
+
"VersionedModelError",
|
19
46
|
"__version__",
|
20
47
|
]
|
pyrmute/_migration_manager.py
CHANGED
@@ -1,12 +1,15 @@
|
|
1
1
|
"""Migrations manager."""
|
2
2
|
|
3
|
+
import contextlib
|
3
4
|
from collections.abc import Callable
|
4
5
|
from typing import Any, Self, get_args, get_origin
|
5
6
|
|
6
7
|
from pydantic import BaseModel
|
7
8
|
from pydantic.fields import FieldInfo
|
9
|
+
from pydantic_core import PydanticUndefined
|
8
10
|
|
9
11
|
from ._registry import Registry
|
12
|
+
from .exceptions import MigrationError, ModelNotFoundError
|
10
13
|
from .model_version import ModelVersion
|
11
14
|
from .types import MigrationData, MigrationFunc, ModelName
|
12
15
|
|
@@ -14,11 +17,11 @@ from .types import MigrationData, MigrationFunc, ModelName
|
|
14
17
|
class MigrationManager:
|
15
18
|
"""Manager for data migrations between model versions.
|
16
19
|
|
17
|
-
Handles registration and execution of migration functions, including
|
18
|
-
|
20
|
+
Handles registration and execution of migration functions, including support for
|
21
|
+
nested Pydantic models.
|
19
22
|
|
20
23
|
Attributes:
|
21
|
-
registry: Reference to the
|
24
|
+
registry: Reference to the Registry.
|
22
25
|
"""
|
23
26
|
|
24
27
|
def __init__(self: Self, registry: Registry) -> None:
|
@@ -87,7 +90,8 @@ class MigrationManager:
|
|
87
90
|
Migrated data dictionary.
|
88
91
|
|
89
92
|
Raises:
|
90
|
-
|
93
|
+
ModelNotFoundError: If model or versions don't exist.
|
94
|
+
MigrationError: If migration path cannot be found.
|
91
95
|
"""
|
92
96
|
from_ver = (
|
93
97
|
ModelVersion.parse(from_version)
|
@@ -103,7 +107,7 @@ class MigrationManager:
|
|
103
107
|
if from_ver == to_ver:
|
104
108
|
return data
|
105
109
|
|
106
|
-
path = self.
|
110
|
+
path = self.find_migration_path(name, from_ver, to_ver)
|
107
111
|
|
108
112
|
current_data = data
|
109
113
|
for i in range(len(path) - 1):
|
@@ -111,15 +115,41 @@ class MigrationManager:
|
|
111
115
|
|
112
116
|
if migration_key in self.registry._migrations[name]:
|
113
117
|
migration_func = self.registry._migrations[name][migration_key]
|
114
|
-
|
118
|
+
try:
|
119
|
+
current_data = migration_func(current_data)
|
120
|
+
except Exception as e:
|
121
|
+
raise MigrationError(
|
122
|
+
name,
|
123
|
+
str(path[i]),
|
124
|
+
str(path[i + 1]),
|
125
|
+
f"Migration function raised: {type(e).__name__}: {e}",
|
126
|
+
) from e
|
127
|
+
elif path[i + 1] in self.registry._backward_compatible_enabled[name]:
|
128
|
+
try:
|
129
|
+
current_data = self._auto_migrate(
|
130
|
+
current_data, name, path[i], path[i + 1]
|
131
|
+
)
|
132
|
+
except Exception as e:
|
133
|
+
raise MigrationError(
|
134
|
+
name,
|
135
|
+
str(path[i]),
|
136
|
+
str(path[i + 1]),
|
137
|
+
f"Auto-migration failed: {type(e).__name__}: {e}",
|
138
|
+
) from e
|
115
139
|
else:
|
116
|
-
|
117
|
-
|
140
|
+
raise MigrationError(
|
141
|
+
name,
|
142
|
+
str(path[i]),
|
143
|
+
str(path[i + 1]),
|
144
|
+
(
|
145
|
+
"No migration path found. Define a migration function or mark "
|
146
|
+
"the target version as backward_compatible."
|
147
|
+
),
|
118
148
|
)
|
119
149
|
|
120
150
|
return current_data
|
121
151
|
|
122
|
-
def
|
152
|
+
def find_migration_path(
|
123
153
|
self: Self,
|
124
154
|
name: ModelName,
|
125
155
|
from_ver: ModelVersion,
|
@@ -134,11 +164,16 @@ class MigrationManager:
|
|
134
164
|
|
135
165
|
Returns:
|
136
166
|
List of versions forming the migration path.
|
167
|
+
|
168
|
+
Raises:
|
169
|
+
ModelNotFoundError: If the model or versions don't exist.
|
137
170
|
"""
|
138
171
|
versions = sorted(self.registry.get_versions(name))
|
139
172
|
|
140
|
-
if from_ver not in versions
|
141
|
-
raise
|
173
|
+
if from_ver not in versions:
|
174
|
+
raise ModelNotFoundError(name, str(from_ver))
|
175
|
+
if to_ver not in versions:
|
176
|
+
raise ModelNotFoundError(name, str(to_ver))
|
142
177
|
|
143
178
|
from_idx = versions.index(from_ver)
|
144
179
|
to_idx = versions.index(to_ver)
|
@@ -147,6 +182,46 @@ class MigrationManager:
|
|
147
182
|
return versions[from_idx : to_idx + 1]
|
148
183
|
return versions[to_idx : from_idx + 1][::-1]
|
149
184
|
|
185
|
+
def validate_migration_path(
|
186
|
+
self: Self,
|
187
|
+
name: ModelName,
|
188
|
+
from_ver: ModelVersion,
|
189
|
+
to_ver: ModelVersion,
|
190
|
+
) -> None:
|
191
|
+
"""Validate that a migration path exists and all steps are valid.
|
192
|
+
|
193
|
+
Args:
|
194
|
+
name: Name of the model.
|
195
|
+
from_ver: Source version.
|
196
|
+
to_ver: Target version.
|
197
|
+
|
198
|
+
Raises:
|
199
|
+
ModelNotFoundError: If the model or versions don't exist.
|
200
|
+
MigrationError: If any step in the migration path is invalid.
|
201
|
+
"""
|
202
|
+
path = self.find_migration_path(name, from_ver, to_ver)
|
203
|
+
|
204
|
+
for i in range(len(path) - 1):
|
205
|
+
current_ver = path[i]
|
206
|
+
next_ver = path[i + 1]
|
207
|
+
migration_key = (current_ver, next_ver)
|
208
|
+
|
209
|
+
has_explicit = migration_key in self.registry._migrations.get(name, {})
|
210
|
+
has_auto = next_ver in self.registry._backward_compatible_enabled.get(
|
211
|
+
name, set()
|
212
|
+
)
|
213
|
+
|
214
|
+
if not has_explicit and not has_auto:
|
215
|
+
raise MigrationError(
|
216
|
+
name,
|
217
|
+
str(current_ver),
|
218
|
+
str(next_ver),
|
219
|
+
(
|
220
|
+
"No migration path found. Define a migration function or mark "
|
221
|
+
"the target version as backward_compatible."
|
222
|
+
),
|
223
|
+
)
|
224
|
+
|
150
225
|
def _auto_migrate(
|
151
226
|
self: Self,
|
152
227
|
data: MigrationData,
|
@@ -156,8 +231,8 @@ class MigrationManager:
|
|
156
231
|
) -> MigrationData:
|
157
232
|
"""Automatically migrate data when no explicit migration exists.
|
158
233
|
|
159
|
-
This method handles nested Pydantic models recursively, migrating
|
160
|
-
|
234
|
+
This method handles nested Pydantic models recursively, migrating them to their
|
235
|
+
corresponding versions.
|
161
236
|
|
162
237
|
Args:
|
163
238
|
data: Data dictionary to migrate.
|
@@ -177,18 +252,25 @@ class MigrationManager:
|
|
177
252
|
result: MigrationData = {}
|
178
253
|
|
179
254
|
for field_name, to_field_info in to_fields.items():
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
255
|
+
# Field exists in data, migrate it
|
256
|
+
if field_name in data:
|
257
|
+
value = data[field_name]
|
258
|
+
from_field_info = from_fields.get(field_name)
|
259
|
+
result[field_name] = self._migrate_field_value(
|
260
|
+
value, from_field_info, to_field_info
|
261
|
+
)
|
184
262
|
|
185
|
-
#
|
186
|
-
|
263
|
+
# Field missing from data, use default if available
|
264
|
+
elif to_field_info.default is not PydanticUndefined:
|
265
|
+
result[field_name] = to_field_info.default
|
266
|
+
elif to_field_info.default_factory is not None:
|
267
|
+
with contextlib.suppress(Exception):
|
268
|
+
result[field_name] = to_field_info.default_factory() # type: ignore
|
187
269
|
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
270
|
+
# Migrate all extra data not in the field, too
|
271
|
+
for field_name, value in data.items():
|
272
|
+
if field_name not in to_fields:
|
273
|
+
result[field_name] = value
|
192
274
|
|
193
275
|
return result
|
194
276
|
|
@@ -211,20 +293,17 @@ class MigrationManager:
|
|
211
293
|
if value is None:
|
212
294
|
return None
|
213
295
|
|
214
|
-
# Check if this is a nested Pydantic model
|
215
296
|
if isinstance(value, dict):
|
216
297
|
nested_info = self._extract_nested_model_info(value, from_field, to_field)
|
217
298
|
if nested_info:
|
218
299
|
nested_name, nested_from_ver, nested_to_ver = nested_info
|
219
300
|
return self.migrate(value, nested_name, nested_from_ver, nested_to_ver)
|
220
301
|
|
221
|
-
# Try to recursively migrate dict values
|
222
302
|
return {
|
223
303
|
k: self._migrate_field_value(v, from_field, to_field)
|
224
304
|
for k, v in value.items()
|
225
305
|
}
|
226
306
|
|
227
|
-
# Handle lists
|
228
307
|
if isinstance(value, list):
|
229
308
|
return [
|
230
309
|
self._migrate_field_value(item, from_field, to_field) for item in value
|
@@ -249,12 +328,10 @@ class MigrationManager:
|
|
249
328
|
Tuple of (model_name, from_version, to_version) if this is a
|
250
329
|
versioned nested model, None otherwise.
|
251
330
|
"""
|
252
|
-
# Get the target model type
|
253
331
|
to_model_type = self._get_model_type_from_field(to_field)
|
254
332
|
if not to_model_type or not issubclass(to_model_type, BaseModel):
|
255
333
|
return None
|
256
334
|
|
257
|
-
# Check if target model is registered
|
258
335
|
to_info = self.registry.get_model_info(to_model_type)
|
259
336
|
if not to_info:
|
260
337
|
return None
|
@@ -291,11 +368,9 @@ class MigrationManager:
|
|
291
368
|
if annotation is None:
|
292
369
|
return None
|
293
370
|
|
294
|
-
# Handle direct model types
|
295
371
|
if isinstance(annotation, type) and issubclass(annotation, BaseModel):
|
296
372
|
return annotation
|
297
373
|
|
298
|
-
# Handle Optional, List, etc.
|
299
374
|
origin = get_origin(annotation)
|
300
375
|
if origin is not None:
|
301
376
|
args = get_args(annotation)
|
pyrmute/_registry.py
CHANGED
@@ -6,6 +6,7 @@ from typing import Self
|
|
6
6
|
|
7
7
|
from pydantic import BaseModel
|
8
8
|
|
9
|
+
from .exceptions import ModelNotFoundError
|
9
10
|
from .model_version import ModelVersion
|
10
11
|
from .types import (
|
11
12
|
DecoratedBaseModel,
|
@@ -21,8 +22,8 @@ from .types import (
|
|
21
22
|
class Registry:
|
22
23
|
"""Registry for versioned Pydantic models.
|
23
24
|
|
24
|
-
Manages the registration and retrieval of versioned models and their
|
25
|
-
|
25
|
+
Manages the registration and retrieval of versioned models and their associated
|
26
|
+
metadata.
|
26
27
|
|
27
28
|
Attributes:
|
28
29
|
_models: Dictionary mapping model names to version-model mappings.
|
@@ -39,6 +40,9 @@ class Registry:
|
|
39
40
|
self._schema_generators: dict[ModelName, SchemaGenerators] = defaultdict(dict)
|
40
41
|
self._model_metadata: dict[type[BaseModel], ModelMetadata] = {}
|
41
42
|
self._ref_enabled: dict[ModelName, set[ModelVersion]] = defaultdict(set)
|
43
|
+
self._backward_compatible_enabled: dict[ModelName, set[ModelVersion]] = (
|
44
|
+
defaultdict(set)
|
45
|
+
)
|
42
46
|
|
43
47
|
def register(
|
44
48
|
self: Self,
|
@@ -46,6 +50,7 @@ class Registry:
|
|
46
50
|
version: str | ModelVersion,
|
47
51
|
schema_generator: JsonSchemaGenerator | None = None,
|
48
52
|
enable_ref: bool = False,
|
53
|
+
backward_compatible: bool = False,
|
49
54
|
) -> Callable[[type[DecoratedBaseModel]], type[DecoratedBaseModel]]:
|
50
55
|
"""Register a versioned model.
|
51
56
|
|
@@ -53,8 +58,11 @@ class Registry:
|
|
53
58
|
name: Name of the model.
|
54
59
|
version: Semantic version string or ModelVersion instance.
|
55
60
|
schema_generator: Optional custom schema generator function.
|
56
|
-
enable_ref: If True, this model can be referenced via $ref in
|
57
|
-
|
61
|
+
enable_ref: If True, this model can be referenced via $ref in separate
|
62
|
+
schema files. If False, it will always be inlined.
|
63
|
+
backward_compatible: If True, this model does not need a migration function
|
64
|
+
to migrate to the next version. If a migration function is defined it
|
65
|
+
will use it.
|
58
66
|
|
59
67
|
Returns:
|
60
68
|
Decorator function for model class.
|
@@ -74,6 +82,8 @@ class Registry:
|
|
74
82
|
self._schema_generators[name][ver] = schema_generator
|
75
83
|
if enable_ref:
|
76
84
|
self._ref_enabled[name].add(ver)
|
85
|
+
if backward_compatible:
|
86
|
+
self._backward_compatible_enabled[name].add(ver)
|
77
87
|
return cls
|
78
88
|
|
79
89
|
return decorator
|
@@ -91,12 +101,15 @@ class Registry:
|
|
91
101
|
Model class for the specified version.
|
92
102
|
|
93
103
|
Raises:
|
94
|
-
|
104
|
+
ModelNotFoundError: If model or version not found.
|
95
105
|
"""
|
96
106
|
ver = ModelVersion.parse(version) if isinstance(version, str) else version
|
97
107
|
|
98
|
-
if name not in self._models
|
99
|
-
raise
|
108
|
+
if name not in self._models:
|
109
|
+
raise ModelNotFoundError(name)
|
110
|
+
|
111
|
+
if ver not in self._models[name]:
|
112
|
+
raise ModelNotFoundError(name, str(ver))
|
100
113
|
|
101
114
|
return self._models[name][ver]
|
102
115
|
|
@@ -110,10 +123,10 @@ class Registry:
|
|
110
123
|
Latest version of the model class.
|
111
124
|
|
112
125
|
Raises:
|
113
|
-
|
126
|
+
ModelNotFoundError: If model not found.
|
114
127
|
"""
|
115
128
|
if name not in self._models:
|
116
|
-
raise
|
129
|
+
raise ModelNotFoundError(name)
|
117
130
|
|
118
131
|
latest_version = max(self._models[name].keys())
|
119
132
|
return self._models[name][latest_version]
|
@@ -128,10 +141,10 @@ class Registry:
|
|
128
141
|
Sorted list of available versions.
|
129
142
|
|
130
143
|
Raises:
|
131
|
-
|
144
|
+
ModelNotFoundError: If model not found.
|
132
145
|
"""
|
133
146
|
if name not in self._models:
|
134
|
-
raise
|
147
|
+
raise ModelNotFoundError(name)
|
135
148
|
|
136
149
|
return sorted(self._models[name].keys())
|
137
150
|
|
pyrmute/_schema_manager.py
CHANGED
@@ -8,6 +8,7 @@ from pydantic import BaseModel
|
|
8
8
|
from pydantic.fields import FieldInfo
|
9
9
|
|
10
10
|
from ._registry import Registry
|
11
|
+
from .exceptions import ModelNotFoundError
|
11
12
|
from .model_version import ModelVersion
|
12
13
|
from .types import (
|
13
14
|
JsonSchema,
|
@@ -21,8 +22,8 @@ from .types import (
|
|
21
22
|
class SchemaManager:
|
22
23
|
"""Manager for JSON schema generation and export.
|
23
24
|
|
24
|
-
Handles schema generation from Pydantic models with support for
|
25
|
-
|
25
|
+
Handles schema generation from Pydantic models with support for custom schema
|
26
|
+
generators and sub-schema references.
|
26
27
|
|
27
28
|
Attributes:
|
28
29
|
registry: Reference to the Registry.
|
@@ -73,14 +74,14 @@ class SchemaManager:
|
|
73
74
|
) -> JsonSchema:
|
74
75
|
"""Get JSON schema with separate definition files for nested models.
|
75
76
|
|
76
|
-
This creates a schema where nested Pydantic models are referenced
|
77
|
-
|
77
|
+
This creates a schema where nested Pydantic models are referenced as external
|
78
|
+
JSON schema files rather than inline definitions.
|
78
79
|
|
79
80
|
Args:
|
80
81
|
name: Name of the model.
|
81
82
|
version: Semantic version.
|
82
|
-
ref_template: Template for generating $ref URLs. Supports {model}
|
83
|
-
|
83
|
+
ref_template: Template for generating $ref URLs. Supports {model} and
|
84
|
+
{version} placeholders.
|
84
85
|
**schema_kwargs: Additional arguments for schema generation.
|
85
86
|
|
86
87
|
Returns:
|
@@ -93,8 +94,6 @@ class SchemaManager:
|
|
93
94
|
... )
|
94
95
|
"""
|
95
96
|
ver = ModelVersion.parse(version) if isinstance(version, str) else version
|
96
|
-
|
97
|
-
# Get the base schema with definitions
|
98
97
|
schema = self.get_schema(name, ver, **schema_kwargs)
|
99
98
|
|
100
99
|
# Extract and replace definitions with external references
|
@@ -136,16 +135,15 @@ class SchemaManager:
|
|
136
135
|
if "$ref" in value:
|
137
136
|
# Extract the definition name from the ref
|
138
137
|
ref = value["$ref"]
|
139
|
-
if ref.startswith(
|
138
|
+
if isinstance(ref, str) and ref.startswith(
|
139
|
+
("#/$defs/", "#/definitions/")
|
140
|
+
):
|
140
141
|
def_name = ref.split("/")[-1]
|
141
142
|
|
142
|
-
# Try to find the model info for this definition
|
143
143
|
model_info = self._find_model_for_definition(def_name)
|
144
|
-
|
145
144
|
if model_info:
|
146
145
|
model_name, model_version = model_info
|
147
146
|
|
148
|
-
# Check if this model is enabled for $ref
|
149
147
|
if self.registry.is_ref_enabled(model_name, model_version):
|
150
148
|
# Replace with external reference
|
151
149
|
return {
|
@@ -156,7 +154,6 @@ class SchemaManager:
|
|
156
154
|
# Keep as internal reference (will be inlined)
|
157
155
|
return value
|
158
156
|
|
159
|
-
# Recursively process nested dictionaries
|
160
157
|
return {k: process_value(v) for k, v in value.items()}
|
161
158
|
if isinstance(value, list):
|
162
159
|
return [process_value(item) for item in value]
|
@@ -178,7 +175,6 @@ class SchemaManager:
|
|
178
175
|
Returns:
|
179
176
|
Dictionary of definitions that weren't converted to external refs.
|
180
177
|
"""
|
181
|
-
# Find all internal refs still in the schema
|
182
178
|
internal_refs: set[str] = set()
|
183
179
|
|
184
180
|
def find_internal_refs(value: dict[str, Any] | list[Any]) -> None:
|
@@ -195,8 +191,6 @@ class SchemaManager:
|
|
195
191
|
find_internal_refs(item)
|
196
192
|
|
197
193
|
find_internal_refs(schema)
|
198
|
-
|
199
|
-
# Return only definitions that are still referenced internally
|
200
194
|
return {k: v for k, v in original_defs.items() if k in internal_refs}
|
201
195
|
|
202
196
|
def _find_model_for_definition(self: Self, def_name: str) -> ModelMetadata | None:
|
@@ -208,7 +202,6 @@ class SchemaManager:
|
|
208
202
|
Returns:
|
209
203
|
Tuple of (model_name, version) if found, None otherwise.
|
210
204
|
"""
|
211
|
-
# Search through all registered models to find matching class name
|
212
205
|
for name, versions in self.registry._models.items():
|
213
206
|
for version, model_class in versions.items():
|
214
207
|
if model_class.__name__ == def_name:
|
@@ -225,10 +218,10 @@ class SchemaManager:
|
|
225
218
|
Dictionary mapping versions to their schemas.
|
226
219
|
|
227
220
|
Raises:
|
228
|
-
|
221
|
+
ModelNotFoundError: If model not found.
|
229
222
|
"""
|
230
223
|
if name not in self.registry._models:
|
231
|
-
raise
|
224
|
+
raise ModelNotFoundError(name)
|
232
225
|
|
233
226
|
return {
|
234
227
|
version: self.get_schema(name, version)
|
@@ -247,8 +240,8 @@ class SchemaManager:
|
|
247
240
|
Args:
|
248
241
|
output_dir: Directory path for output files.
|
249
242
|
indent: JSON indentation level.
|
250
|
-
separate_definitions: If True, create separate schema files for
|
251
|
-
|
243
|
+
separate_definitions: If True, create separate schema files for nested
|
244
|
+
models that have enable_ref=True.
|
252
245
|
ref_template: Template for $ref URLs when separate_definitions=True.
|
253
246
|
Defaults to relative file references if not provided.
|
254
247
|
|
@@ -270,19 +263,15 @@ class SchemaManager:
|
|
270
263
|
output_path.mkdir(parents=True, exist_ok=True)
|
271
264
|
|
272
265
|
if not separate_definitions:
|
273
|
-
# Original behavior: inline definitions
|
274
266
|
for name in self.registry._models:
|
275
267
|
for version, schema in self.get_all_schemas(name).items():
|
276
268
|
file_path = output_path / f"{name}_v{version}.json"
|
277
269
|
with open(file_path, "w", encoding="utf-8") as f:
|
278
270
|
json.dump(schema, f, indent=indent)
|
279
271
|
else:
|
280
|
-
# New behavior: separate definition files for enable_ref=True models
|
281
|
-
# Default to relative file references
|
282
272
|
if ref_template is None:
|
283
273
|
ref_template = "{model}_v{version}.json"
|
284
274
|
|
285
|
-
# First pass: write all schemas
|
286
275
|
for name in self.registry._models:
|
287
276
|
for version in self.registry._models[name]:
|
288
277
|
schema = self.get_schema_with_separate_defs(
|
@@ -335,11 +324,9 @@ class SchemaManager:
|
|
335
324
|
if annotation is None:
|
336
325
|
return None
|
337
326
|
|
338
|
-
# Handle direct model types
|
339
327
|
if isinstance(annotation, type) and issubclass(annotation, BaseModel):
|
340
328
|
return annotation
|
341
329
|
|
342
|
-
# Handle Optional, List, etc.
|
343
330
|
origin = get_origin(annotation)
|
344
331
|
if origin is not None:
|
345
332
|
args = get_args(annotation)
|
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.2.0'
|
32
|
+
__version_tuple__ = version_tuple = (0, 2, 0)
|
33
33
|
|
34
34
|
__commit_id__ = commit_id = None
|
pyrmute/exceptions.py
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
"""Exceptions."""
|
2
|
+
|
3
|
+
from typing import Self
|
4
|
+
|
5
|
+
|
6
|
+
class VersionedModelError(Exception):
|
7
|
+
"""Base exception for all versioned model errors."""
|
8
|
+
|
9
|
+
|
10
|
+
class ModelNotFoundError(VersionedModelError):
|
11
|
+
"""Raised when a model or version cannot be found in the registry."""
|
12
|
+
|
13
|
+
def __init__(self: Self, name: str, version: str | None = None) -> None:
|
14
|
+
"""Initializes ModelNotFoundError."""
|
15
|
+
self.name = name
|
16
|
+
self.version = version
|
17
|
+
if version:
|
18
|
+
msg = f"Model '{name}' version '{version}' not found in registry"
|
19
|
+
else:
|
20
|
+
msg = f"Model '{name}' not found in registry"
|
21
|
+
super().__init__(msg)
|
22
|
+
|
23
|
+
|
24
|
+
class MigrationError(VersionedModelError):
|
25
|
+
"""Raised when a migration fails or cannot be found."""
|
26
|
+
|
27
|
+
def __init__(
|
28
|
+
self: Self,
|
29
|
+
name: str,
|
30
|
+
from_version: str,
|
31
|
+
to_version: str,
|
32
|
+
reason: str | None = None,
|
33
|
+
) -> None:
|
34
|
+
"""Initializes MigrationError."""
|
35
|
+
self.name = name
|
36
|
+
self.from_version = from_version
|
37
|
+
self.to_version = to_version
|
38
|
+
self.reason = reason
|
39
|
+
|
40
|
+
msg = f"Migration failed for '{name}': {from_version} → {to_version}"
|
41
|
+
if reason:
|
42
|
+
msg += f"\nReason: {reason}"
|
43
|
+
super().__init__(msg)
|
44
|
+
|
45
|
+
|
46
|
+
class InvalidVersionError(VersionedModelError):
|
47
|
+
"""Raised when a version string cannot be parsed."""
|
48
|
+
|
49
|
+
def __init__(self: Self, version_string: str, reason: str | None = None) -> None:
|
50
|
+
"""Initializes InvalidVersionError."""
|
51
|
+
self.version_string = version_string
|
52
|
+
msg = f"Invalid version string: '{version_string}'"
|
53
|
+
if reason:
|
54
|
+
msg += f"\n{reason}"
|
55
|
+
super().__init__(msg)
|