"""Pulp JSON → 화면용 표시 모델 변환 (순수 함수, httpx 의존 없음). GPG 검증 배지 로직(✅/⚠️/⚪)은 화면 4 에서도 재사용하므로 여기 함수로 둔다(스펙 §6 화면4). """ from __future__ import annotations from typing import Any def gpg_status(repo: dict[str, Any]) -> dict[str, str]: """repo_config 의 gpgcheck / repo-gpgcheck 로 3단계 검증 배지 산출. - pass(✅): 패키지 서명 + repo 메타데이터 서명 모두 검증 - warn(⚠️): 둘 중 하나만 검증 - unset(⚪): 검증 미설정 """ cfg = repo.get("repo_config") or {} pkg = bool(cfg.get("gpgcheck", 0)) meta = bool(cfg.get("repo-gpgcheck", 0)) # level 은 색상 칩(badge-{level})으로 렌더 — 장식 이모지는 쓰지 않는다(design-ref §7). if pkg and meta: return {"level": "pass", "label": "검증됨"} if pkg or meta: return {"level": "warn", "label": "부분 검증"} return {"level": "unset", "label": "미설정"} def version_number(version_href: str | None) -> int | None: """latest_version_href 끝의 정수(vN) 추출. 예: .../versions/3/ → 3""" if not version_href: return None parts = [p for p in version_href.split("/") if p] try: return int(parts[-1]) except (ValueError, IndexError): return None def _content_count(version: dict[str, Any], section: str) -> int: """content_summary[section] (present/added/removed) 의 카운트 합계.""" data = (version.get("content_summary") or {}).get(section) or {} return sum((info or {}).get("count", 0) for info in data.values()) def package_count(version: dict[str, Any] | None) -> int | None: """RepositoryVersion 의 content_summary.present 카운트 합계.""" if version is None: return None return _content_count(version, "present") def _uuid_from_href(href: str) -> str: parts = [p for p in href.split("/") if p] return parts[-1] if parts else "" def build_repo_view( repo: dict[str, Any], version: dict[str, Any] | None = None ) -> dict[str, Any]: """저장소 1건 + (선택) 최신 버전 상세 → 대시보드 행 모델.""" return { "name": repo.get("name", ""), "uuid": _uuid_from_href(repo.get("pulp_href", "")), "href": repo.get("pulp_href", ""), "latest_version": version_number(repo.get("latest_version_href")), "package_count": package_count(version), # 최신 스냅샷 생성 시각 ≈ 마지막 동기화 시각 "last_sync": (version or {}).get("pulp_created"), "gpg": gpg_status(repo), # TODO(단계3/4): 현재 운영 배포 버전은 Distribution→publication→version 역추적 필요. # 단계1에선 최신 버전만 표시한다. "deployed_version": None, } def build_version_view( version: dict[str, Any], deployed_number: int | None = None, gpg: dict[str, str] | None = None, ) -> dict[str, Any]: """RepositoryVersion → 버전 목록 행 모델. gpg 검증은 저장소 단위 설정(repo_config)이라 모든 버전이 같은 값을 공유한다 — 호출부에서 repo 의 gpg_status() 를 주입한다. """ number = version_number(version.get("pulp_href")) is_deployed = number is not None and number == deployed_number # 배포 가능: 검증 통과(pass) + 현재 배포 중이 아님 (스펙 §5-2 게이트) deployable = (gpg or {}).get("level") == "pass" and not is_deployed return { "number": number, "href": version.get("pulp_href"), "created": version.get("pulp_created"), "package_count": _content_count(version, "present"), "added": _content_count(version, "added"), "removed": _content_count(version, "removed"), "gpg": gpg, "is_deployed": is_deployed, "deployable": deployable, } def deploy_preview( version_views: list[dict[str, Any]], target_number: int | None, deployed_number: int | None, ) -> dict[str, Any]: """배포 확인 모달용 미리보기: 현재 운영 버전 → 대상 버전, 순 패키지 변화.""" def find(n: int | None) -> dict[str, Any] | None: return next((v for v in version_views if v["number"] == n), None) current = find(deployed_number) target = find(target_number) current_count = current["package_count"] if current else 0 target_count = target["package_count"] if target else 0 net = target_count - current_count return { "current_version": deployed_number, "current_count": current_count, "target_version": target_number, "target_count": target_count, "net": abs(net), "net_sign": "+" if net >= 0 else "−", } def verification_detail(repo: dict[str, Any]) -> dict[str, Any]: """검증 상태 상세 (화면 4): GPG/checksum 설정값 + 종합 배지.""" cfg = repo.get("repo_config") or {} return { "status": gpg_status(repo), "gpgcheck": int(bool(cfg.get("gpgcheck", 0))), "repo_gpgcheck": int(bool(cfg.get("repo-gpgcheck", 0))), "checksum_type": repo.get("metadata_checksum_type") or cfg.get("checksum_type") or "—", } def summarize(repo_views: list[dict[str, Any]], syncing: int = 0) -> dict[str, int]: """상단 요약 카드 수치. syncing 은 task_store.active_count() 에서 주입.""" return { "total": len(repo_views), "syncing": syncing, "gpg_pass": sum(1 for r in repo_views if r["gpg"]["level"] == "pass"), "total_packages": sum(r["package_count"] or 0 for r in repo_views), } def task_progress(task: dict[str, Any]) -> dict[str, Any]: """Pulp task → 진행률 표시 모델. progress_reports[] 의 done/total 을 합산해 퍼센트를 낸다. 완료 시 created_resources 에서 새 RepositoryVersion 번호(vN)를 추출한다. """ state = task.get("state", "waiting") reports = task.get("progress_reports") or [] done = sum((r or {}).get("done") or 0 for r in reports) total = sum((r or {}).get("total") or 0 for r in reports) percent = int(done / total * 100) if total else 0 new_version = None for href in task.get("created_resources") or []: if "/versions/" in href: new_version = version_number(href) break error = None if state == "failed": err = task.get("error") or {} error = err.get("description") or "동기화 작업이 실패했습니다." return { "state": state, "done": done, "total": total, "percent": percent, "new_version": new_version, "error": error, "completed": state == "completed", "failed": state == "failed", "running": state in ("waiting", "running"), }