pyrmute 0.1.0__py3-none-any.whl → 0.3.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 +35 -8
- pyrmute/_migration_manager.py +112 -37
- pyrmute/_registry.py +24 -11
- pyrmute/_schema_manager.py +31 -34
- 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 +529 -68
- pyrmute/model_version.py +16 -8
- pyrmute/types.py +17 -5
- pyrmute-0.3.0.dist-info/METADATA +352 -0
- pyrmute-0.3.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.3.0.dist-info}/WHEEL +0 -0
- {pyrmute-0.1.0.dist-info → pyrmute-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {pyrmute-0.1.0.dist-info → pyrmute-0.3.0.dist-info}/top_level.txt +0 -0
pyrmute/__init__.py
CHANGED
@@ -1,20 +1,47 @@
|
|
1
|
-
"""pyrmute - versioned Pydantic models and schemas with migrations.
|
2
|
-
|
3
|
-
A package for managing versioned Pydantic models with automatic migrations
|
4
|
-
and schema management.
|
5
|
-
"""
|
1
|
+
"""pyrmute - versioned Pydantic models and schemas with migrations."""
|
6
2
|
|
3
|
+
from ._migration_manager import MigrationManager
|
4
|
+
from ._registry import Registry
|
5
|
+
from ._schema_manager import SchemaManager
|
7
6
|
from ._version import __version__
|
7
|
+
from .exceptions import (
|
8
|
+
InvalidVersionError,
|
9
|
+
MigrationError,
|
10
|
+
ModelNotFoundError,
|
11
|
+
VersionedModelError,
|
12
|
+
)
|
13
|
+
from .migration_testing import (
|
14
|
+
MigrationTestCase,
|
15
|
+
MigrationTestResult,
|
16
|
+
MigrationTestResults,
|
17
|
+
)
|
18
|
+
from .model_diff import ModelDiff
|
8
19
|
from .model_manager import ModelManager
|
9
20
|
from .model_version import ModelVersion
|
10
|
-
from .types import
|
21
|
+
from .types import (
|
22
|
+
JsonSchema,
|
23
|
+
MigrationFunc,
|
24
|
+
ModelData,
|
25
|
+
NestedModelInfo,
|
26
|
+
)
|
11
27
|
|
12
28
|
__all__ = [
|
29
|
+
"InvalidVersionError",
|
13
30
|
"JsonSchema",
|
14
|
-
"
|
31
|
+
"MigrationError",
|
15
32
|
"MigrationFunc",
|
33
|
+
"MigrationManager",
|
34
|
+
"MigrationTestCase",
|
35
|
+
"MigrationTestResult",
|
36
|
+
"MigrationTestResults",
|
37
|
+
"ModelData",
|
38
|
+
"ModelDiff",
|
16
39
|
"ModelManager",
|
17
|
-
"
|
40
|
+
"ModelNotFoundError",
|
18
41
|
"ModelVersion",
|
42
|
+
"NestedModelInfo",
|
43
|
+
"Registry",
|
44
|
+
"SchemaManager",
|
45
|
+
"VersionedModelError",
|
19
46
|
"__version__",
|
20
47
|
]
|
pyrmute/_migration_manager.py
CHANGED
@@ -1,24 +1,27 @@
|
|
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
|
-
from .types import
|
14
|
+
from .types import MigrationFunc, ModelData, ModelName
|
12
15
|
|
13
16
|
|
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:
|
@@ -70,11 +73,11 @@ class MigrationManager:
|
|
70
73
|
|
71
74
|
def migrate(
|
72
75
|
self: Self,
|
73
|
-
data:
|
76
|
+
data: ModelData,
|
74
77
|
name: ModelName,
|
75
78
|
from_version: str | ModelVersion,
|
76
79
|
to_version: str | ModelVersion,
|
77
|
-
) ->
|
80
|
+
) -> ModelData:
|
78
81
|
"""Migrate data from one version to another.
|
79
82
|
|
80
83
|
Args:
|
@@ -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,17 +182,57 @@ 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
|
-
data:
|
227
|
+
data: ModelData,
|
153
228
|
name: ModelName,
|
154
229
|
from_ver: ModelVersion,
|
155
230
|
to_ver: ModelVersion,
|
156
|
-
) ->
|
231
|
+
) -> ModelData:
|
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.
|
@@ -174,21 +249,28 @@ class MigrationManager:
|
|
174
249
|
from_fields = from_model.model_fields
|
175
250
|
to_fields = to_model.model_fields
|
176
251
|
|
177
|
-
result:
|
252
|
+
result: ModelData = {}
|
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
|
@@ -234,7 +313,7 @@ class MigrationManager:
|
|
234
313
|
|
235
314
|
def _extract_nested_model_info(
|
236
315
|
self: Self,
|
237
|
-
value:
|
316
|
+
value: ModelData,
|
238
317
|
from_field: FieldInfo | None,
|
239
318
|
to_field: FieldInfo,
|
240
319
|
) -> tuple[ModelName, ModelVersion, ModelVersion] | None:
|
@@ -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,
|
@@ -15,14 +16,15 @@ from .types import (
|
|
15
16
|
JsonValue,
|
16
17
|
ModelMetadata,
|
17
18
|
ModelName,
|
19
|
+
NestedModelInfo,
|
18
20
|
)
|
19
21
|
|
20
22
|
|
21
23
|
class SchemaManager:
|
22
24
|
"""Manager for JSON schema generation and export.
|
23
25
|
|
24
|
-
Handles schema generation from Pydantic models with support for
|
25
|
-
|
26
|
+
Handles schema generation from Pydantic models with support for custom schema
|
27
|
+
generators and sub-schema references.
|
26
28
|
|
27
29
|
Attributes:
|
28
30
|
registry: Reference to the Registry.
|
@@ -73,14 +75,14 @@ class SchemaManager:
|
|
73
75
|
) -> JsonSchema:
|
74
76
|
"""Get JSON schema with separate definition files for nested models.
|
75
77
|
|
76
|
-
This creates a schema where nested Pydantic models are referenced
|
77
|
-
|
78
|
+
This creates a schema where nested Pydantic models are referenced as external
|
79
|
+
JSON schema files rather than inline definitions.
|
78
80
|
|
79
81
|
Args:
|
80
82
|
name: Name of the model.
|
81
83
|
version: Semantic version.
|
82
|
-
ref_template: Template for generating $ref URLs. Supports {model}
|
83
|
-
|
84
|
+
ref_template: Template for generating $ref URLs. Supports {model} and
|
85
|
+
{version} placeholders.
|
84
86
|
**schema_kwargs: Additional arguments for schema generation.
|
85
87
|
|
86
88
|
Returns:
|
@@ -93,8 +95,6 @@ class SchemaManager:
|
|
93
95
|
... )
|
94
96
|
"""
|
95
97
|
ver = ModelVersion.parse(version) if isinstance(version, str) else version
|
96
|
-
|
97
|
-
# Get the base schema with definitions
|
98
98
|
schema = self.get_schema(name, ver, **schema_kwargs)
|
99
99
|
|
100
100
|
# Extract and replace definitions with external references
|
@@ -136,16 +136,15 @@ class SchemaManager:
|
|
136
136
|
if "$ref" in value:
|
137
137
|
# Extract the definition name from the ref
|
138
138
|
ref = value["$ref"]
|
139
|
-
if ref.startswith(
|
139
|
+
if isinstance(ref, str) and ref.startswith(
|
140
|
+
("#/$defs/", "#/definitions/")
|
141
|
+
):
|
140
142
|
def_name = ref.split("/")[-1]
|
141
143
|
|
142
|
-
# Try to find the model info for this definition
|
143
144
|
model_info = self._find_model_for_definition(def_name)
|
144
|
-
|
145
145
|
if model_info:
|
146
146
|
model_name, model_version = model_info
|
147
147
|
|
148
|
-
# Check if this model is enabled for $ref
|
149
148
|
if self.registry.is_ref_enabled(model_name, model_version):
|
150
149
|
# Replace with external reference
|
151
150
|
return {
|
@@ -156,7 +155,6 @@ class SchemaManager:
|
|
156
155
|
# Keep as internal reference (will be inlined)
|
157
156
|
return value
|
158
157
|
|
159
|
-
# Recursively process nested dictionaries
|
160
158
|
return {k: process_value(v) for k, v in value.items()}
|
161
159
|
if isinstance(value, list):
|
162
160
|
return [process_value(item) for item in value]
|
@@ -178,7 +176,6 @@ class SchemaManager:
|
|
178
176
|
Returns:
|
179
177
|
Dictionary of definitions that weren't converted to external refs.
|
180
178
|
"""
|
181
|
-
# Find all internal refs still in the schema
|
182
179
|
internal_refs: set[str] = set()
|
183
180
|
|
184
181
|
def find_internal_refs(value: dict[str, Any] | list[Any]) -> None:
|
@@ -195,8 +192,6 @@ class SchemaManager:
|
|
195
192
|
find_internal_refs(item)
|
196
193
|
|
197
194
|
find_internal_refs(schema)
|
198
|
-
|
199
|
-
# Return only definitions that are still referenced internally
|
200
195
|
return {k: v for k, v in original_defs.items() if k in internal_refs}
|
201
196
|
|
202
197
|
def _find_model_for_definition(self: Self, def_name: str) -> ModelMetadata | None:
|
@@ -208,7 +203,6 @@ class SchemaManager:
|
|
208
203
|
Returns:
|
209
204
|
Tuple of (model_name, version) if found, None otherwise.
|
210
205
|
"""
|
211
|
-
# Search through all registered models to find matching class name
|
212
206
|
for name, versions in self.registry._models.items():
|
213
207
|
for version, model_class in versions.items():
|
214
208
|
if model_class.__name__ == def_name:
|
@@ -225,10 +219,10 @@ class SchemaManager:
|
|
225
219
|
Dictionary mapping versions to their schemas.
|
226
220
|
|
227
221
|
Raises:
|
228
|
-
|
222
|
+
ModelNotFoundError: If model not found.
|
229
223
|
"""
|
230
224
|
if name not in self.registry._models:
|
231
|
-
raise
|
225
|
+
raise ModelNotFoundError(name)
|
232
226
|
|
233
227
|
return {
|
234
228
|
version: self.get_schema(name, version)
|
@@ -247,8 +241,8 @@ class SchemaManager:
|
|
247
241
|
Args:
|
248
242
|
output_dir: Directory path for output files.
|
249
243
|
indent: JSON indentation level.
|
250
|
-
separate_definitions: If True, create separate schema files for
|
251
|
-
|
244
|
+
separate_definitions: If True, create separate schema files for nested
|
245
|
+
models that have enable_ref=True.
|
252
246
|
ref_template: Template for $ref URLs when separate_definitions=True.
|
253
247
|
Defaults to relative file references if not provided.
|
254
248
|
|
@@ -270,19 +264,15 @@ class SchemaManager:
|
|
270
264
|
output_path.mkdir(parents=True, exist_ok=True)
|
271
265
|
|
272
266
|
if not separate_definitions:
|
273
|
-
# Original behavior: inline definitions
|
274
267
|
for name in self.registry._models:
|
275
268
|
for version, schema in self.get_all_schemas(name).items():
|
276
269
|
file_path = output_path / f"{name}_v{version}.json"
|
277
270
|
with open(file_path, "w", encoding="utf-8") as f:
|
278
271
|
json.dump(schema, f, indent=indent)
|
279
272
|
else:
|
280
|
-
# New behavior: separate definition files for enable_ref=True models
|
281
|
-
# Default to relative file references
|
282
273
|
if ref_template is None:
|
283
274
|
ref_template = "{model}_v{version}.json"
|
284
275
|
|
285
|
-
# First pass: write all schemas
|
286
276
|
for name in self.registry._models:
|
287
277
|
for version in self.registry._models[name]:
|
288
278
|
schema = self.get_schema_with_separate_defs(
|
@@ -296,7 +286,7 @@ class SchemaManager:
|
|
296
286
|
self: Self,
|
297
287
|
name: ModelName,
|
298
288
|
version: str | ModelVersion,
|
299
|
-
) -> list[
|
289
|
+
) -> list[NestedModelInfo]:
|
300
290
|
"""Get all nested models referenced by a model.
|
301
291
|
|
302
292
|
Args:
|
@@ -304,19 +294,28 @@ class SchemaManager:
|
|
304
294
|
version: Semantic version.
|
305
295
|
|
306
296
|
Returns:
|
307
|
-
List of
|
297
|
+
List of NestedModelInfo.
|
308
298
|
"""
|
309
299
|
ver = ModelVersion.parse(version) if isinstance(version, str) else version
|
310
300
|
model = self.registry.get_model(name, ver)
|
311
301
|
|
312
|
-
nested: list[
|
302
|
+
nested: list[NestedModelInfo] = []
|
313
303
|
|
314
304
|
for field_info in model.model_fields.values():
|
315
305
|
model_type = self._get_model_type_from_field(field_info)
|
316
|
-
if model_type:
|
317
|
-
|
318
|
-
|
319
|
-
|
306
|
+
if not model_type:
|
307
|
+
continue
|
308
|
+
|
309
|
+
model_info = self.registry.get_model_info(model_type)
|
310
|
+
|
311
|
+
if not model_info:
|
312
|
+
continue
|
313
|
+
|
314
|
+
name_, version_ = model_info
|
315
|
+
nested_model_info = NestedModelInfo(name=name_, version=version_)
|
316
|
+
|
317
|
+
if nested_model_info not in nested:
|
318
|
+
nested.append(nested_model_info)
|
320
319
|
|
321
320
|
return nested
|
322
321
|
|
@@ -335,11 +334,9 @@ class SchemaManager:
|
|
335
334
|
if annotation is None:
|
336
335
|
return None
|
337
336
|
|
338
|
-
# Handle direct model types
|
339
337
|
if isinstance(annotation, type) and issubclass(annotation, BaseModel):
|
340
338
|
return annotation
|
341
339
|
|
342
|
-
# Handle Optional, List, etc.
|
343
340
|
origin = get_origin(annotation)
|
344
341
|
if origin is not None:
|
345
342
|
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.3.0'
|
32
|
+
__version_tuple__ = version_tuple = (0, 3, 0)
|
33
33
|
|
34
34
|
__commit_id__ = commit_id = None
|