Source code for production_configuration.compatibility

"""Validate models against the actual production runtime before submission."""

import inspect
import json
import logging
import os
import subprocess
import sys
from pathlib import Path

from simtools import __version__, dependency_versions, version
from simtools.model_repository.compatibility import validate_release

IMAGE_KEY = "container_image"
CATALOG_KEY = "catalog"
TARGET_KEY = "target_simtools"
PATH_KEY = "simulation_models_path"
GIT_PATH_KEY = "simulation_models_git_path"
GIT_REVISION_KEY = "simulation_models_git_revision"
logger = logging.getLogger(__name__)


def _bundled_catalog_path():
    """Locate a runtime's own catalog without consulting its working directory."""
    filename = dependency_versions.DEPENDENCY_VERSIONS_FILENAME
    for path in (
        Path(dependency_versions.__file__).resolve().parents[2] / filename,
        Path(sys.prefix) / "simtools" / filename,
    ):
        if path.is_file():
            return path
    raise RuntimeError("Target installation has no bundled dependency catalog.")


# Runs inside the target image: read its own catalog and report simtools version and catalog.
INFO_CODE = (
    "import os, sys\nfrom pathlib import Path\nfrom simtools import dependency_versions\n"
    + inspect.getsource(_bundled_catalog_path)
    + "\ncatalog_path = _bundled_catalog_path()\n"
    + "os.environ['SIMTOOLS_DEPENDENCY_VERSIONS'] = str(catalog_path)\n"
    + "import json\nfrom simtools import __version__\n"
    + "print(json.dumps({'target_simtools': __version__, 'catalog': "
    + "dependency_versions.load_dependency_catalog(catalog_path, validate=False)}))\n"
)


def _target_runtime(image):
    """Return the target simtools version and catalog, from an image or the host."""
    if not image:
        return {
            TARGET_KEY: __version__,
            CATALOG_KEY: dependency_versions.load_dependency_catalog(_bundled_catalog_path()),
        }
    env = {
        key: value
        for key, value in os.environ.items()
        if not key.endswith(("PYTHONPATH", "PYTHONHOME", "SIMTOOLS_DEPENDENCY_VERSIONS"))
    }
    result = subprocess.run(
        ["apptainer", "exec", "--cleanenv", str(image), "python", "-P", "-c", INFO_CODE],
        check=True,
        capture_output=True,
        text=True,
        env=env,
    )
    return json.loads(result.stdout)


[docs] def preview(args, image=None): """Check selected models using the target installation's bundled catalog.""" target = _target_runtime(image) path = args.get(PATH_KEY) or args.get(GIT_PATH_KEY) if not path: raise ValueError("Production requires an explicit simulation-model source path.") revision = None if not args.get(PATH_KEY): revision = args.get(GIT_REVISION_KEY) if revision is None: revision = dependency_versions.model_repository_revision(target[CATALOG_KEY]) logger.info("Checking models for target simtools %s (host %s)", target[TARGET_KEY], __version__) return { **validate_release( path, target[CATALOG_KEY], revision, version.is_development_build or bool(args.get("test")), ), TARGET_KEY: target[TARGET_KEY], }
[docs] def prepare_jobs(jobs, args, options): """Reject incompatible models before submitting a production grid.""" if jobs: image = options.backend_config.get(IMAGE_KEY) if any(job.resources.get(IMAGE_KEY, image) != image for job in jobs): raise ValueError("Submit separate production grids for different target images.") preview(args, image) return jobs