"""Generate browsable module-directory artifacts from a release catalog.""" from __future__ import annotations from datetime import UTC, datetime import json from pathlib import Path import re from typing import Any from .catalog import DEFAULT_PUBLIC_BASE_URL, canonical_hash from .release_intelligence import compatibility_rows, module_rows, signature_rows def write_module_directory( *, catalog_payload: dict[str, Any], keyring_payload: dict[str, Any], output_root: Path, channel: str, public_base_url: str = DEFAULT_PUBLIC_BASE_URL, ) -> tuple[Path, ...]: payloads = module_directory_payloads( catalog_payload=catalog_payload, keyring_payload=keyring_payload, channel=channel, public_base_url=public_base_url, ) written: list[Path] = [] for relative_path, payload in payloads: path = output_root / relative_path path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8") written.append(path) return tuple(written) def module_directory_payloads( *, catalog_payload: dict[str, Any], keyring_payload: dict[str, Any], channel: str, public_base_url: str = DEFAULT_PUBLIC_BASE_URL, ) -> tuple[tuple[Path, dict[str, Any]], ...]: generated_at = json_datetime(datetime.now(tz=UTC)) modules = module_rows(catalog_payload) compatibility = compatibility_rows(modules) signatures = signature_rows(catalog_payload, keyring_payload) base_url = public_base_url.rstrip("/") catalog_url = f"{base_url}/catalogs/v1/channels/{channel}.json" keyring_url = f"{base_url}/catalogs/v1/keyring.json" catalog_hash = canonical_hash(catalog_payload) catalog_sequence = catalog_payload.get("sequence") files: list[tuple[Path, dict[str, Any]]] = [] module_index_entries: list[dict[str, Any]] = [] for module in modules: module_id = str(module.get("module_id") or module.get("repo") or "") if not module_id: continue version = str(module.get("version") or "0.0.0") module_slug = safe_path_part(module_id) version_slug = safe_path_part(version) module_base = f"{base_url}/catalogs/v1/modules/{module_slug}" manifest_url = f"{module_base}/{version_slug}/manifest.json" module_index_url = f"{module_base}/index.json" module_compatibility = [row for row in compatibility if row.get("module_id") == module_id] manifest = { "manifest_version": "1", "generated_at": generated_at, "channel": channel, "module": module, "compatibility": module_compatibility, "release": { "catalog_url": catalog_url, "keyring_url": keyring_url, "catalog_hash": catalog_hash, "catalog_sequence": catalog_sequence, "signatures": signatures, }, } files.append((Path("modules") / module_slug / version_slug / "manifest.json", manifest)) files.append( ( Path("modules") / module_slug / "index.json", { "index_version": "1", "generated_at": generated_at, "channel": channel, "module_id": module_id, "name": module.get("name"), "latest_version": version, "latest_manifest_url": manifest_url, "versions": [ { "version": version, "manifest_url": manifest_url, "python_tag": module.get("python_tag"), "webui_tag": module.get("webui_tag"), } ], "release": { "catalog_url": catalog_url, "keyring_url": keyring_url, "catalog_hash": catalog_hash, "catalog_sequence": catalog_sequence, }, }, ) ) module_index_entries.append( { "module_id": module_id, "name": module.get("name"), "latest_version": version, "index_url": module_index_url, "latest_manifest_url": manifest_url, "repo": module.get("repo"), } ) files.append( ( Path("modules") / "index.json", { "index_version": "1", "generated_at": generated_at, "channel": channel, "catalog_url": catalog_url, "keyring_url": keyring_url, "catalog_hash": catalog_hash, "catalog_sequence": catalog_sequence, "module_count": len(module_index_entries), "modules": sorted(module_index_entries, key=lambda item: str(item.get("module_id"))), }, ) ) return tuple(files) def safe_path_part(value: str) -> str: return re.sub(r"[^A-Za-z0-9_.-]+", "_", value.strip()) or "_" def json_datetime(value: datetime) -> str: return value.replace(microsecond=0).isoformat().replace("+00:00", "Z")