pyrmute 0.1.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 +20 -0
- pyrmute/_migration_manager.py +306 -0
- pyrmute/_registry.py +172 -0
- pyrmute/_schema_manager.py +350 -0
- pyrmute/_version.py +34 -0
- pyrmute/model_manager.py +264 -0
- pyrmute/model_version.py +60 -0
- pyrmute/py.typed +0 -0
- pyrmute/types.py +26 -0
- pyrmute-0.1.0.dist-info/METADATA +130 -0
- pyrmute-0.1.0.dist-info/RECORD +14 -0
- pyrmute-0.1.0.dist-info/WHEEL +5 -0
- pyrmute-0.1.0.dist-info/licenses/LICENSE +21 -0
- pyrmute-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,350 @@
|
|
1
|
+
"""Schema manager."""
|
2
|
+
|
3
|
+
import json
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Any, Self, get_args, get_origin
|
6
|
+
|
7
|
+
from pydantic import BaseModel
|
8
|
+
from pydantic.fields import FieldInfo
|
9
|
+
|
10
|
+
from ._registry import Registry
|
11
|
+
from .model_version import ModelVersion
|
12
|
+
from .types import (
|
13
|
+
JsonSchema,
|
14
|
+
JsonSchemaDefinitions,
|
15
|
+
JsonValue,
|
16
|
+
ModelMetadata,
|
17
|
+
ModelName,
|
18
|
+
)
|
19
|
+
|
20
|
+
|
21
|
+
class SchemaManager:
|
22
|
+
"""Manager for JSON schema generation and export.
|
23
|
+
|
24
|
+
Handles schema generation from Pydantic models with support for
|
25
|
+
custom schema generators and sub-schema references.
|
26
|
+
|
27
|
+
Attributes:
|
28
|
+
registry: Reference to the Registry.
|
29
|
+
"""
|
30
|
+
|
31
|
+
def __init__(self: Self, registry: Registry) -> None:
|
32
|
+
"""Initialize the schema manager.
|
33
|
+
|
34
|
+
Args:
|
35
|
+
registry: Registry instance to use.
|
36
|
+
"""
|
37
|
+
self.registry = registry
|
38
|
+
|
39
|
+
def get_schema(
|
40
|
+
self: Self,
|
41
|
+
name: ModelName,
|
42
|
+
version: str | ModelVersion,
|
43
|
+
**schema_kwargs: Any,
|
44
|
+
) -> JsonSchema:
|
45
|
+
"""Get JSON schema for a specific model version.
|
46
|
+
|
47
|
+
Args:
|
48
|
+
name: Name of the model.
|
49
|
+
version: Semantic version.
|
50
|
+
**schema_kwargs: Additional arguments for schema generation.
|
51
|
+
|
52
|
+
Returns:
|
53
|
+
JSON schema dictionary.
|
54
|
+
"""
|
55
|
+
ver = ModelVersion.parse(version) if isinstance(version, str) else version
|
56
|
+
model = self.registry.get_model(name, ver)
|
57
|
+
|
58
|
+
if (
|
59
|
+
name in self.registry._schema_generators
|
60
|
+
and ver in self.registry._schema_generators[name]
|
61
|
+
):
|
62
|
+
generator = self.registry._schema_generators[name][ver]
|
63
|
+
return generator(model)
|
64
|
+
|
65
|
+
return model.model_json_schema(**schema_kwargs)
|
66
|
+
|
67
|
+
def get_schema_with_separate_defs(
|
68
|
+
self: Self,
|
69
|
+
name: ModelName,
|
70
|
+
version: str | ModelVersion,
|
71
|
+
ref_template: str = "{model}_v{version}.json",
|
72
|
+
**schema_kwargs: Any,
|
73
|
+
) -> JsonSchema:
|
74
|
+
"""Get JSON schema with separate definition files for nested models.
|
75
|
+
|
76
|
+
This creates a schema where nested Pydantic models are referenced
|
77
|
+
as external JSON schema files rather than inline definitions.
|
78
|
+
|
79
|
+
Args:
|
80
|
+
name: Name of the model.
|
81
|
+
version: Semantic version.
|
82
|
+
ref_template: Template for generating $ref URLs. Supports {model}
|
83
|
+
and {version} placeholders.
|
84
|
+
**schema_kwargs: Additional arguments for schema generation.
|
85
|
+
|
86
|
+
Returns:
|
87
|
+
JSON schema dictionary with external $ref for nested models.
|
88
|
+
|
89
|
+
Example:
|
90
|
+
>>> schema = manager.get_schema_with_separate_defs(
|
91
|
+
... "User", "2.0.0",
|
92
|
+
... ref_template="https://example.com/schemas/{model}_v{version}.json"
|
93
|
+
... )
|
94
|
+
"""
|
95
|
+
ver = ModelVersion.parse(version) if isinstance(version, str) else version
|
96
|
+
|
97
|
+
# Get the base schema with definitions
|
98
|
+
schema = self.get_schema(name, ver, **schema_kwargs)
|
99
|
+
|
100
|
+
# Extract and replace definitions with external references
|
101
|
+
if "$defs" in schema or "definitions" in schema:
|
102
|
+
defs_key = "$defs" if "$defs" in schema else "definitions"
|
103
|
+
definitions: JsonSchemaDefinitions = schema.pop(defs_key, {}) # type: ignore[assignment]
|
104
|
+
|
105
|
+
# Update all $ref in the schema to point to external files
|
106
|
+
schema = self._replace_refs_with_external(schema, definitions, ref_template)
|
107
|
+
|
108
|
+
# Re-add definitions that weren't converted to external refs
|
109
|
+
remaining_defs = self._get_remaining_defs(schema, definitions)
|
110
|
+
if remaining_defs:
|
111
|
+
schema[defs_key] = remaining_defs
|
112
|
+
|
113
|
+
return schema
|
114
|
+
|
115
|
+
def _replace_refs_with_external(
|
116
|
+
self: Self,
|
117
|
+
schema: JsonSchema,
|
118
|
+
definitions: JsonSchemaDefinitions,
|
119
|
+
ref_template: str,
|
120
|
+
) -> JsonSchema:
|
121
|
+
"""Replace internal $ref with external references.
|
122
|
+
|
123
|
+
Only replaces refs for models that have enable_ref=True.
|
124
|
+
|
125
|
+
Args:
|
126
|
+
schema: The schema to process.
|
127
|
+
definitions: Dictionary of definitions to replace.
|
128
|
+
ref_template: Template for external references.
|
129
|
+
|
130
|
+
Returns:
|
131
|
+
Updated schema with external references.
|
132
|
+
"""
|
133
|
+
|
134
|
+
def process_value(value: JsonValue) -> JsonValue:
|
135
|
+
if isinstance(value, dict):
|
136
|
+
if "$ref" in value:
|
137
|
+
# Extract the definition name from the ref
|
138
|
+
ref = value["$ref"]
|
139
|
+
if ref.startswith(("#/$defs/", "#/definitions/")):
|
140
|
+
def_name = ref.split("/")[-1]
|
141
|
+
|
142
|
+
# Try to find the model info for this definition
|
143
|
+
model_info = self._find_model_for_definition(def_name)
|
144
|
+
|
145
|
+
if model_info:
|
146
|
+
model_name, model_version = model_info
|
147
|
+
|
148
|
+
# Check if this model is enabled for $ref
|
149
|
+
if self.registry.is_ref_enabled(model_name, model_version):
|
150
|
+
# Replace with external reference
|
151
|
+
return {
|
152
|
+
"$ref": ref_template.format(
|
153
|
+
model=model_name, version=str(model_version)
|
154
|
+
)
|
155
|
+
}
|
156
|
+
# Keep as internal reference (will be inlined)
|
157
|
+
return value
|
158
|
+
|
159
|
+
# Recursively process nested dictionaries
|
160
|
+
return {k: process_value(v) for k, v in value.items()}
|
161
|
+
if isinstance(value, list):
|
162
|
+
return [process_value(item) for item in value]
|
163
|
+
return value
|
164
|
+
|
165
|
+
return process_value(schema) # type: ignore[return-value]
|
166
|
+
|
167
|
+
def _get_remaining_defs(
|
168
|
+
self: Self,
|
169
|
+
schema: JsonSchema,
|
170
|
+
original_defs: JsonSchemaDefinitions,
|
171
|
+
) -> JsonSchemaDefinitions:
|
172
|
+
"""Get definitions that should remain inline.
|
173
|
+
|
174
|
+
Args:
|
175
|
+
schema: The processed schema.
|
176
|
+
original_defs: Original definitions.
|
177
|
+
|
178
|
+
Returns:
|
179
|
+
Dictionary of definitions that weren't converted to external refs.
|
180
|
+
"""
|
181
|
+
# Find all internal refs still in the schema
|
182
|
+
internal_refs: set[str] = set()
|
183
|
+
|
184
|
+
def find_internal_refs(value: dict[str, Any] | list[Any]) -> None:
|
185
|
+
if isinstance(value, dict):
|
186
|
+
if "$ref" in value:
|
187
|
+
ref = value["$ref"]
|
188
|
+
if ref.startswith(("#/$defs/", "#/definitions/")):
|
189
|
+
def_name = ref.split("/")[-1]
|
190
|
+
internal_refs.add(def_name)
|
191
|
+
for v in value.values():
|
192
|
+
find_internal_refs(v)
|
193
|
+
elif isinstance(value, list):
|
194
|
+
for item in value:
|
195
|
+
find_internal_refs(item)
|
196
|
+
|
197
|
+
find_internal_refs(schema)
|
198
|
+
|
199
|
+
# Return only definitions that are still referenced internally
|
200
|
+
return {k: v for k, v in original_defs.items() if k in internal_refs}
|
201
|
+
|
202
|
+
def _find_model_for_definition(self: Self, def_name: str) -> ModelMetadata | None:
|
203
|
+
"""Find the registered model corresponding to a definition name.
|
204
|
+
|
205
|
+
Args:
|
206
|
+
def_name: The definition name from the schema.
|
207
|
+
|
208
|
+
Returns:
|
209
|
+
Tuple of (model_name, version) if found, None otherwise.
|
210
|
+
"""
|
211
|
+
# Search through all registered models to find matching class name
|
212
|
+
for name, versions in self.registry._models.items():
|
213
|
+
for version, model_class in versions.items():
|
214
|
+
if model_class.__name__ == def_name:
|
215
|
+
return (name, version)
|
216
|
+
return None
|
217
|
+
|
218
|
+
def get_all_schemas(self: Self, name: ModelName) -> dict[ModelVersion, JsonSchema]:
|
219
|
+
"""Get all schemas for a model across all versions.
|
220
|
+
|
221
|
+
Args:
|
222
|
+
name: Name of the model.
|
223
|
+
|
224
|
+
Returns:
|
225
|
+
Dictionary mapping versions to their schemas.
|
226
|
+
|
227
|
+
Raises:
|
228
|
+
ValueError: If model not found.
|
229
|
+
"""
|
230
|
+
if name not in self.registry._models:
|
231
|
+
raise ValueError(f"Model {name} not found")
|
232
|
+
|
233
|
+
return {
|
234
|
+
version: self.get_schema(name, version)
|
235
|
+
for version in self.registry._models[name]
|
236
|
+
}
|
237
|
+
|
238
|
+
def dump_schemas(
|
239
|
+
self: Self,
|
240
|
+
output_dir: str | Path,
|
241
|
+
indent: int = 2,
|
242
|
+
separate_definitions: bool = False,
|
243
|
+
ref_template: str | None = None,
|
244
|
+
) -> None:
|
245
|
+
"""Dump all schemas to JSON files.
|
246
|
+
|
247
|
+
Args:
|
248
|
+
output_dir: Directory path for output files.
|
249
|
+
indent: JSON indentation level.
|
250
|
+
separate_definitions: If True, create separate schema files for
|
251
|
+
nested models that have enable_ref=True.
|
252
|
+
ref_template: Template for $ref URLs when separate_definitions=True.
|
253
|
+
Defaults to relative file references if not provided.
|
254
|
+
|
255
|
+
Example:
|
256
|
+
>>> # Inline definitions (default)
|
257
|
+
>>> manager.dump_schemas("schemas/")
|
258
|
+
>>>
|
259
|
+
>>> # Separate sub-schemas with relative refs (when enable_ref=True models)
|
260
|
+
>>> manager.dump_schemas("schemas/", separate_definitions=True)
|
261
|
+
>>>
|
262
|
+
>>> # Separate sub-schemas with absolute URLs
|
263
|
+
>>> manager.dump_schemas(
|
264
|
+
... "schemas/",
|
265
|
+
... separate_definitions=True,
|
266
|
+
... ref_template="https://example.com/schemas/{model}_v{version}.json"
|
267
|
+
... )
|
268
|
+
"""
|
269
|
+
output_path = Path(output_dir)
|
270
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
271
|
+
|
272
|
+
if not separate_definitions:
|
273
|
+
# Original behavior: inline definitions
|
274
|
+
for name in self.registry._models:
|
275
|
+
for version, schema in self.get_all_schemas(name).items():
|
276
|
+
file_path = output_path / f"{name}_v{version}.json"
|
277
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
278
|
+
json.dump(schema, f, indent=indent)
|
279
|
+
else:
|
280
|
+
# New behavior: separate definition files for enable_ref=True models
|
281
|
+
# Default to relative file references
|
282
|
+
if ref_template is None:
|
283
|
+
ref_template = "{model}_v{version}.json"
|
284
|
+
|
285
|
+
# First pass: write all schemas
|
286
|
+
for name in self.registry._models:
|
287
|
+
for version in self.registry._models[name]:
|
288
|
+
schema = self.get_schema_with_separate_defs(
|
289
|
+
name, version, ref_template
|
290
|
+
)
|
291
|
+
file_path = output_path / f"{name}_v{version}.json"
|
292
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
293
|
+
json.dump(schema, f, indent=indent)
|
294
|
+
|
295
|
+
def get_nested_models(
|
296
|
+
self: Self,
|
297
|
+
name: ModelName,
|
298
|
+
version: str | ModelVersion,
|
299
|
+
) -> list[ModelMetadata]:
|
300
|
+
"""Get all nested models referenced by a model.
|
301
|
+
|
302
|
+
Args:
|
303
|
+
name: Name of the model.
|
304
|
+
version: Semantic version.
|
305
|
+
|
306
|
+
Returns:
|
307
|
+
List of (model_name, model_version) tuples for nested models.
|
308
|
+
"""
|
309
|
+
ver = ModelVersion.parse(version) if isinstance(version, str) else version
|
310
|
+
model = self.registry.get_model(name, ver)
|
311
|
+
|
312
|
+
nested: list[ModelMetadata] = []
|
313
|
+
|
314
|
+
for field_info in model.model_fields.values():
|
315
|
+
model_type = self._get_model_type_from_field(field_info)
|
316
|
+
if model_type:
|
317
|
+
model_info = self.registry.get_model_info(model_type)
|
318
|
+
if model_info and model_info not in nested:
|
319
|
+
nested.append(model_info)
|
320
|
+
|
321
|
+
return nested
|
322
|
+
|
323
|
+
def _get_model_type_from_field(
|
324
|
+
self: Self, field: FieldInfo
|
325
|
+
) -> type[BaseModel] | None:
|
326
|
+
"""Extract the Pydantic model type from a field.
|
327
|
+
|
328
|
+
Args:
|
329
|
+
field: The field info to extract from.
|
330
|
+
|
331
|
+
Returns:
|
332
|
+
The model type if found, None otherwise.
|
333
|
+
"""
|
334
|
+
annotation = field.annotation
|
335
|
+
if annotation is None:
|
336
|
+
return None
|
337
|
+
|
338
|
+
# Handle direct model types
|
339
|
+
if isinstance(annotation, type) and issubclass(annotation, BaseModel):
|
340
|
+
return annotation
|
341
|
+
|
342
|
+
# Handle Optional, List, etc.
|
343
|
+
origin = get_origin(annotation)
|
344
|
+
if origin is not None:
|
345
|
+
args = get_args(annotation)
|
346
|
+
for arg in args:
|
347
|
+
if isinstance(arg, type) and issubclass(arg, BaseModel):
|
348
|
+
return arg
|
349
|
+
|
350
|
+
return None
|
pyrmute/_version.py
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# file generated by setuptools-scm
|
2
|
+
# don't change, don't track in version control
|
3
|
+
|
4
|
+
__all__ = [
|
5
|
+
"__version__",
|
6
|
+
"__version_tuple__",
|
7
|
+
"version",
|
8
|
+
"version_tuple",
|
9
|
+
"__commit_id__",
|
10
|
+
"commit_id",
|
11
|
+
]
|
12
|
+
|
13
|
+
TYPE_CHECKING = False
|
14
|
+
if TYPE_CHECKING:
|
15
|
+
from typing import Tuple
|
16
|
+
from typing import Union
|
17
|
+
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
19
|
+
COMMIT_ID = Union[str, None]
|
20
|
+
else:
|
21
|
+
VERSION_TUPLE = object
|
22
|
+
COMMIT_ID = object
|
23
|
+
|
24
|
+
version: str
|
25
|
+
__version__: str
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
27
|
+
version_tuple: VERSION_TUPLE
|
28
|
+
commit_id: COMMIT_ID
|
29
|
+
__commit_id__: COMMIT_ID
|
30
|
+
|
31
|
+
__version__ = version = '0.1.0'
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 0)
|
33
|
+
|
34
|
+
__commit_id__ = commit_id = None
|
pyrmute/model_manager.py
ADDED
@@ -0,0 +1,264 @@
|
|
1
|
+
"""Model manager."""
|
2
|
+
|
3
|
+
from collections.abc import Callable
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Any, Self
|
6
|
+
|
7
|
+
from pydantic import BaseModel
|
8
|
+
|
9
|
+
from ._migration_manager import MigrationManager
|
10
|
+
from ._registry import Registry
|
11
|
+
from ._schema_manager import SchemaManager
|
12
|
+
from .model_version import ModelVersion
|
13
|
+
from .types import (
|
14
|
+
DecoratedBaseModel,
|
15
|
+
JsonSchema,
|
16
|
+
JsonSchemaGenerator,
|
17
|
+
MigrationData,
|
18
|
+
MigrationFunc,
|
19
|
+
ModelMetadata,
|
20
|
+
)
|
21
|
+
|
22
|
+
|
23
|
+
class ModelManager:
|
24
|
+
"""High-level interface for versioned model management.
|
25
|
+
|
26
|
+
Provides a unified API for model registration, migration, and schema
|
27
|
+
management.
|
28
|
+
|
29
|
+
Attributes:
|
30
|
+
registry: Registry instance.
|
31
|
+
migration_manager: MigrationManager instance.
|
32
|
+
schema_manager: SchemaManager instance.
|
33
|
+
|
34
|
+
Example:
|
35
|
+
>>> manager = ModelManager()
|
36
|
+
>>>
|
37
|
+
>>> @manager.model("User", "1.0.0")
|
38
|
+
... class UserV1(BaseModel):
|
39
|
+
... name: str
|
40
|
+
>>>
|
41
|
+
>>> @manager.model("User", "2.0.0")
|
42
|
+
... class UserV2(BaseModel):
|
43
|
+
... name: str
|
44
|
+
... email: str
|
45
|
+
>>>
|
46
|
+
>>> @manager.migration("User", "1.0.0", "2.0.0")
|
47
|
+
... def migrate(data: MigrationData) -> MigrationData:
|
48
|
+
... return {**data, "email": "unknown@example.com"}
|
49
|
+
"""
|
50
|
+
|
51
|
+
def __init__(self: Self) -> None:
|
52
|
+
"""Initialize the versioned model manager."""
|
53
|
+
self.registry = Registry()
|
54
|
+
self.migration_manager = MigrationManager(self.registry)
|
55
|
+
self.schema_manager = SchemaManager(self.registry)
|
56
|
+
|
57
|
+
def model(
|
58
|
+
self: Self,
|
59
|
+
name: str,
|
60
|
+
version: str | ModelVersion,
|
61
|
+
schema_generator: JsonSchemaGenerator | None = None,
|
62
|
+
enable_ref: bool = False,
|
63
|
+
) -> Callable[[type[DecoratedBaseModel]], type[DecoratedBaseModel]]:
|
64
|
+
"""Register a versioned model.
|
65
|
+
|
66
|
+
Args:
|
67
|
+
name: Name of the model.
|
68
|
+
version: Semantic version.
|
69
|
+
schema_generator: Optional custom schema generator.
|
70
|
+
enable_ref: If True, this model can be referenced via $ref in
|
71
|
+
separate schema files. If False, it will always be inlined.
|
72
|
+
|
73
|
+
Returns:
|
74
|
+
Decorator function for model class.
|
75
|
+
|
76
|
+
Example:
|
77
|
+
>>> # Model that will be inlined (default)
|
78
|
+
>>> @manager.model("Address", "1.0.0")
|
79
|
+
... class AddressV1(BaseModel):
|
80
|
+
... street: str
|
81
|
+
>>>
|
82
|
+
>>> # Model that can be a separate schema with $ref
|
83
|
+
>>> @manager.model("City", "1.0.0", enable_ref=True)
|
84
|
+
... class CityV1(BaseModel):
|
85
|
+
... city: City
|
86
|
+
"""
|
87
|
+
return self.registry.register(name, version, schema_generator, enable_ref)
|
88
|
+
|
89
|
+
def migration(
|
90
|
+
self: Self,
|
91
|
+
name: str,
|
92
|
+
from_version: str | ModelVersion,
|
93
|
+
to_version: str | ModelVersion,
|
94
|
+
) -> Callable[[MigrationFunc], MigrationFunc]:
|
95
|
+
"""Register a migration function.
|
96
|
+
|
97
|
+
Args:
|
98
|
+
name: Name of the model.
|
99
|
+
from_version: Source version.
|
100
|
+
to_version: Target version.
|
101
|
+
|
102
|
+
Returns:
|
103
|
+
Decorator function for migration function.
|
104
|
+
"""
|
105
|
+
return self.migration_manager.register_migration(name, from_version, to_version)
|
106
|
+
|
107
|
+
def get(
|
108
|
+
self: Self, name: str, version: str | ModelVersion | None = None
|
109
|
+
) -> type[BaseModel]:
|
110
|
+
"""Get a model by name and version.
|
111
|
+
|
112
|
+
Args:
|
113
|
+
name: Name of the model.
|
114
|
+
version: Semantic version (returns latest if None).
|
115
|
+
|
116
|
+
Returns:
|
117
|
+
Model class.
|
118
|
+
"""
|
119
|
+
if version is None:
|
120
|
+
return self.registry.get_latest(name)
|
121
|
+
return self.registry.get_model(name, version)
|
122
|
+
|
123
|
+
def migrate(
|
124
|
+
self: Self,
|
125
|
+
data: MigrationData,
|
126
|
+
name: str,
|
127
|
+
from_version: str | ModelVersion,
|
128
|
+
to_version: str | ModelVersion,
|
129
|
+
) -> BaseModel:
|
130
|
+
"""Migrate data between versions.
|
131
|
+
|
132
|
+
Args:
|
133
|
+
data: Data dictionary or BaseModel to migrate.
|
134
|
+
name: Name of the model.
|
135
|
+
from_version: Source version.
|
136
|
+
to_version: Target version.
|
137
|
+
|
138
|
+
Returns:
|
139
|
+
Migrated BaseModel.
|
140
|
+
"""
|
141
|
+
migrated_data = self.migration_manager.migrate(
|
142
|
+
data, name, from_version, to_version
|
143
|
+
)
|
144
|
+
target_model = self.get(name, to_version)
|
145
|
+
return target_model.model_validate(migrated_data)
|
146
|
+
|
147
|
+
def get_schema(
|
148
|
+
self: Self,
|
149
|
+
name: str,
|
150
|
+
version: str | ModelVersion,
|
151
|
+
**kwargs: Any,
|
152
|
+
) -> JsonSchema:
|
153
|
+
"""Get JSON schema for a specific version.
|
154
|
+
|
155
|
+
Args:
|
156
|
+
name: Name of the model.
|
157
|
+
version: Semantic version.
|
158
|
+
**kwargs: Additional schema generation arguments.
|
159
|
+
|
160
|
+
Returns:
|
161
|
+
JSON schema dictionary.
|
162
|
+
"""
|
163
|
+
return self.schema_manager.get_schema(name, version, **kwargs)
|
164
|
+
|
165
|
+
def list_models(self: Self) -> list[str]:
|
166
|
+
"""Get list of all registered models.
|
167
|
+
|
168
|
+
Returns:
|
169
|
+
List of model names.
|
170
|
+
"""
|
171
|
+
return self.registry.list_models()
|
172
|
+
|
173
|
+
def list_versions(self: Self, name: str) -> list[ModelVersion]:
|
174
|
+
"""Get all versions for a model.
|
175
|
+
|
176
|
+
Args:
|
177
|
+
name: Name of the model.
|
178
|
+
|
179
|
+
Returns:
|
180
|
+
Sorted list of versions.
|
181
|
+
"""
|
182
|
+
return self.registry.get_versions(name)
|
183
|
+
|
184
|
+
def dump_schemas(
|
185
|
+
self: Self,
|
186
|
+
output_dir: str | Path,
|
187
|
+
indent: int = 2,
|
188
|
+
separate_definitions: bool = False,
|
189
|
+
ref_template: str | None = None,
|
190
|
+
) -> None:
|
191
|
+
"""Export all schemas to JSON files.
|
192
|
+
|
193
|
+
Args:
|
194
|
+
output_dir: Directory path for output.
|
195
|
+
indent: JSON indentation level.
|
196
|
+
separate_definitions: If True, create separate schema files for
|
197
|
+
nested models and use $ref to reference them.
|
198
|
+
ref_template: Template for $ref URLs when separate_definitions=True.
|
199
|
+
Defaults to relative file references if not provided.
|
200
|
+
|
201
|
+
Example:
|
202
|
+
>>> # Inline definitions (default)
|
203
|
+
>>> manager.dump_schemas("schemas/")
|
204
|
+
>>>
|
205
|
+
>>> # Separate sub-schemas with relative refs
|
206
|
+
>>> manager.dump_schemas("schemas/", separate_definitions=True)
|
207
|
+
>>>
|
208
|
+
>>> # Separate sub-schemas with absolute URLs
|
209
|
+
>>> manager.dump_schemas(
|
210
|
+
... "schemas/",
|
211
|
+
... separate_definitions=True,
|
212
|
+
... ref_template="https://example.com/schemas/{model}_v{version}.json"
|
213
|
+
... )
|
214
|
+
"""
|
215
|
+
self.schema_manager.dump_schemas(
|
216
|
+
output_dir, indent, separate_definitions, ref_template
|
217
|
+
)
|
218
|
+
|
219
|
+
def dump_schemas_with_refs(
|
220
|
+
self: Self,
|
221
|
+
output_dir: str | Path,
|
222
|
+
ref_template: str | None = None,
|
223
|
+
indent: int = 2,
|
224
|
+
) -> None:
|
225
|
+
"""Export schemas with separate files for nested models.
|
226
|
+
|
227
|
+
This is a convenience method that calls dump_schemas with
|
228
|
+
separate_definitions=True.
|
229
|
+
|
230
|
+
Args:
|
231
|
+
output_dir: Directory path for output.
|
232
|
+
ref_template: Template for $ref URLs. Supports {model} and
|
233
|
+
{version} placeholders. Defaults to relative file refs.
|
234
|
+
indent: JSON indentation level.
|
235
|
+
|
236
|
+
Example:
|
237
|
+
>>> # Relative file references (default)
|
238
|
+
>>> manager.dump_schemas_with_refs("schemas/")
|
239
|
+
>>>
|
240
|
+
>>> # Absolute URL references
|
241
|
+
>>> manager.dump_schemas_with_refs(
|
242
|
+
... "schemas/",
|
243
|
+
... ref_template="https://example.com/schemas/{model}_v{version}.json"
|
244
|
+
... )
|
245
|
+
"""
|
246
|
+
self.schema_manager.dump_schemas(
|
247
|
+
output_dir, indent, separate_definitions=True, ref_template=ref_template
|
248
|
+
)
|
249
|
+
|
250
|
+
def get_nested_models(
|
251
|
+
self: Self,
|
252
|
+
name: str,
|
253
|
+
version: str | ModelVersion,
|
254
|
+
) -> list[ModelMetadata]:
|
255
|
+
"""Get all nested models used by a model.
|
256
|
+
|
257
|
+
Args:
|
258
|
+
name: Name of the model.
|
259
|
+
version: Semantic version.
|
260
|
+
|
261
|
+
Returns:
|
262
|
+
List of (model_name, version) tuples for nested models.
|
263
|
+
"""
|
264
|
+
return self.schema_manager.get_nested_models(name, version)
|
pyrmute/model_version.py
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
"""Models the version provided to managers."""
|
2
|
+
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from typing import Self
|
5
|
+
|
6
|
+
|
7
|
+
@dataclass(frozen=True, order=True)
|
8
|
+
class ModelVersion:
|
9
|
+
"""Semantic version representation.
|
10
|
+
|
11
|
+
Attributes:
|
12
|
+
major: Major version number (breaking changes).
|
13
|
+
minor: Minor version number (backward-compatible features).
|
14
|
+
patch: Patch version number (backward-compatible fixes).
|
15
|
+
"""
|
16
|
+
|
17
|
+
major: int
|
18
|
+
minor: int
|
19
|
+
patch: int
|
20
|
+
|
21
|
+
@classmethod
|
22
|
+
def parse(cls, version_str: str) -> Self:
|
23
|
+
"""Parse a semantic version string.
|
24
|
+
|
25
|
+
Args:
|
26
|
+
version_str: Version string in format "major.minor.patch".
|
27
|
+
|
28
|
+
Returns:
|
29
|
+
Parsed Version instance.
|
30
|
+
|
31
|
+
Raises:
|
32
|
+
ValueError: If version string format is invalid.
|
33
|
+
"""
|
34
|
+
parts = version_str.split(".")
|
35
|
+
if len(parts) != 3: # noqa: PLR2004
|
36
|
+
raise ValueError(f"Invalid version format: {version_str}")
|
37
|
+
|
38
|
+
try:
|
39
|
+
major, minor, patch = int(parts[0]), int(parts[1]), int(parts[2])
|
40
|
+
if major < 0 or minor < 0 or patch < 0:
|
41
|
+
raise ValueError(f"Invalid version format: {version_str}")
|
42
|
+
return cls(major, minor, patch)
|
43
|
+
except ValueError as e:
|
44
|
+
raise ValueError(f"Invalid version format: {version_str}") from e
|
45
|
+
|
46
|
+
def __str__(self: Self) -> str:
|
47
|
+
"""Return string representation of version.
|
48
|
+
|
49
|
+
Returns:
|
50
|
+
Version string in format "major.minor.patch".
|
51
|
+
"""
|
52
|
+
return f"{self.major}.{self.minor}.{self.patch}"
|
53
|
+
|
54
|
+
def __repr__(self: Self) -> str:
|
55
|
+
"""Return detailed string representation.
|
56
|
+
|
57
|
+
Returns:
|
58
|
+
Detailed version representation.
|
59
|
+
"""
|
60
|
+
return f"ModelVersion({self.major}, {self.minor}, {self.patch})"
|