Source code for repoman.config_io

"""Save and load .repoman state files (pure JSON, no GTK, fully unit-testable).

File format v1: JSON with version, saved_at, and a repos list.
Each entry captures enough to reconstruct a Repository and detect drift
when the same config is loaded on a different machine or after a reinstall.
"""

from __future__ import annotations

import json
from datetime import datetime
from pathlib import Path

from .models import FileFormat, Repository


[docs] def save_config(repos: list[Repository]) -> str: """Serialise repo list to a JSON string for writing to a .repoman file.""" return json.dumps( { "version": 1, "saved_at": datetime.now().isoformat(timespec="seconds"), "repos": [ { "types": r.types, "uris": r.uris, "suites": r.suites, "components": r.components, "enabled": r.enabled, "description": r.description, "signed_by": r.signed_by, "architectures": r.architectures, "source_file": str(r.source_file), } for r in repos ], }, indent=2, )
[docs] def load_config(path: Path) -> list[dict]: """Parse a .repoman JSON file and return the list of repo entries. :param path: Path to the .repoman file to read. :type path: Path :returns: List of raw repo dicts from the ``repos`` key. :rtype: list[dict] :raises json.JSONDecodeError: File is not valid JSON. :raises ValueError: ``version`` field is missing or not ``1``. :raises KeyError: Required top-level key is absent. """ data = json.loads(path.read_text(encoding="utf-8")) if data.get("version") != 1: raise ValueError(f"Unsupported config version: {data.get('version')!r}") return data["repos"]
[docs] def match_repos( saved: list[dict], live: list[Repository], ) -> tuple[list[tuple[dict, Repository]], list[dict]]: """Match saved config entries to live repos by primary URI. :param saved: List of raw repo dicts from a loaded .repoman file. :type saved: list[dict] :param live: Currently loaded repositories from the system. :type live: list[Repository] :returns: A 2-tuple of ``(matched, missing)``, where ``matched`` is a list of ``(saved_entry, live_repo)`` pairs for repos found on the system, and ``missing`` is a list of saved entries whose URI was not found. :rtype: tuple[list[tuple[dict, Repository]], list[dict]] """ live_by_uri = {r.uris[0]: r for r in live if r.uris} matched: list[tuple[dict, Repository]] = [] missing: list[dict] = [] for entry in saved: uris = entry.get("uris") or [] uri = uris[0] if uris else None if uri and uri in live_by_uri: matched.append((entry, live_by_uri[uri])) else: missing.append(entry) return matched, missing
[docs] def entry_to_repository(entry: dict) -> Repository: """Reconstruct a Repository from a saved config entry. Used when creating repos that exist in the config but not on the system. Always produces DEB822 format; source_file is taken from the saved entry. """ return Repository( source_file=Path(entry["source_file"]), file_format=FileFormat.DEB822, types=entry.get("types") or ["deb"], uris=entry["uris"], suites=entry.get("suites") or [], components=entry.get("components") or [], enabled=entry.get("enabled", True), description=entry.get("description"), signed_by=entry.get("signed_by"), architectures=entry.get("architectures") or [], )