"""FlowDeck — v5.2.0 Forge abstraction (Gitea / GitHub). ``ForgeAdapter`` defines the minimal contract a forge client must expose for the ``projects`` table sync and the issue/board integration. ``GiteaAdapter`` wraps the existing ``GiteaClient``; ``GitHubAdapter`` (in ``github_adapter.py``) is the GitHub implementation. """ from __future__ import annotations from abc import ABC, abstractmethod class ForgeAdapter(ABC): """Common forge API surface used by FlowDeck (v5.2.0).""" kind = "base" @abstractmethod async def validate_token(self) -> bool: """True when the stored credentials still work.""" @abstractmethod async def list_repos(self, page: int = 1) -> list[dict]: """List repositories for the authenticated user.""" @abstractmethod async def get_repo_info(self, owner: str, repo: str) -> dict: """Repository metadata (default_branch, clone_url, language, …).""" def normalize_repo(repo: dict, proj_type: str) -> dict: """Project a forge repo dict onto the ``projects`` table columns.""" full_name = repo.get("full_name", "") or repo.get("fullName", "") owner, _, name = full_name.partition("/") return { "name": name or repo.get("name", ""), "owner": owner or repo.get("owner", {}).get("login", "") if isinstance(repo.get("owner"), dict) else (owner or ""), "proj_type": proj_type, "forge_id": str(repo.get("id", "") or ""), "clone_url": repo.get("clone_url", "") or repo.get("ssh_url", ""), "default_branch": repo.get("default_branch", ""), "language": repo.get("language", ""), "description": (repo.get("description") or "") or "", } class GiteaAdapter(ForgeAdapter): """Adapt the existing GiteaClient to the ForgeAdapter contract.""" kind = "gitea" def __init__(self, client) -> None: # client = GiteaClient instance self._client = client async def validate_token(self) -> bool: try: await self._client.get_user_repos(page=1, limit=1) return True except Exception: return False async def list_repos(self, page: int = 1) -> list[dict]: return await self._client.get_user_repos(page=page, limit=30) async def get_repo_info(self, owner: str, repo: str) -> dict: async with __import__("httpx").AsyncClient(timeout=15) as client: resp = await client.get( f"{self._client._base}/repos/{owner}/{repo}", headers=self._client._headers, ) resp.raise_for_status() return resp.json()