Source code for deploy_tools.models.save_and_load

import json
from collections import defaultdict
from pathlib import Path
from typing import BinaryIO, TextIO

import yaml
from pydantic import BaseModel
from pydantic import ValidationError as PydanticValidationError

from ..errors import DeployToolsError
from .deployment import (
    Deployment,
    DeploymentSettings,
    ReleasesByNameAndVersion,
)
from .module import Module, Release

YAML_FILE_SUFFIX = ".yaml"
DEPLOYMENT_SETTINGS = "settings" + YAML_FILE_SUFFIX


[docs] class LoadError(DeployToolsError): """Raised when configuration files cannot be loaded into the model."""
[docs] def save_as_yaml( obj: BaseModel, output_path: Path, create_parents: bool = False ) -> None: """Serialise a Pydantic model to YAML and write it to the given path.""" if create_parents: output_path.parent.mkdir(exist_ok=True, parents=True) with open(output_path, "w") as f: # Use model_dump_json() and then yaml.safe_dump() to allow us to use Pydantic # types such as AnyURL with YAML without needing a custom serializer load_json = json.loads(obj.model_dump_json()) yaml.safe_dump(load_json, f)
[docs] def load_from_yaml[T: BaseModel](model: type[T], input_stream: TextIO | BinaryIO) -> T: """Load a single Pydantic model from a yaml file.""" return model(**yaml.safe_load(input_stream))
[docs] def load_deployment(config_folder: Path) -> Deployment: """Load Deployment configuration from a yaml file.""" with open(config_folder / DEPLOYMENT_SETTINGS) as f: settings = load_from_yaml(DeploymentSettings, f) releases: ReleasesByNameAndVersion = defaultdict(dict) for version_path in config_folder.glob("*/*"): release = _load_release(version_path) module = release.module # This also guarantees unique module names and versions in configuration _check_filepath_matches(version_path, module) name = module.name version = module.version releases[name][version] = release return Deployment(settings=settings, releases=releases)
def _load_release(path: Path) -> Release: """Load Module configuration from a yaml file.""" if path.is_dir() or not path.suffix == YAML_FILE_SUFFIX: raise LoadError(f"Unexpected file in configuration directory:\n{path}") try: with open(path) as f: return load_from_yaml(Release, f) except (OSError, yaml.YAMLError, PydanticValidationError, TypeError) as exc: # Include exc: under the top-level handler the traceback (and chained cause) is # suppressed, so the failing field is only visible if surfaced in the message. raise LoadError(f"Module configuration is invalid:\n{path}\n{exc}") from exc def _check_filepath_matches(version_path: Path, module: Module) -> None: """Ensure the Module's file path (in config folder) matches the metadata. It should be the Module's name and version as /<config_folder>/<name>/<version>.yaml """ if not module.name == version_path.parent.name: raise LoadError( f"Module name {module.name} does not match path:\n{version_path}" ) version_match = ( module.version == version_path.name or version_path.suffix == YAML_FILE_SUFFIX and module.version == version_path.stem ) if not version_match: raise LoadError( f"Module version {module.version} does not match path:\n{version_path}" )