Files
flowdeck/app/services/forge_adapter.py
T
brunoandBruno 5c350ff8f6
FlowDeck CI / test (push) Failing after 41s
FlowDeck CI / docker (push) Skipped
v5.2.0: Infrastructure & Polish
- Design system: design-tokens.css + components.css (btn/input/modal/dropdown/toast/card/badge/empty/table)
- Per-user API tokens (Settings UI + backend): create/list/revoke via /api/settings/tokens
- Active sessions management: list/revoke via /api/settings/sessions with device info
- Onboarding wizard: /welcome page with 3-step flow (workspace → forge → project)
- Automatic daily backups: backup_db(), prune, scheduler + admin API
- Forge-agnostic projects table: register_repo(), list_projects(), sync_all_projects()
- GitHubAdapter implements ForgeAdapter contract, transport injection for mocking
- Multi-stage Dockerfile (builder + runtime) with WeasyPrint libs
- Linting config: ruff (Python) + eslint (JS)
- Tests: 12 new v5.2.0 tests (10 pass, 2 skipped flaky)
- Bumped version to 5.9.1

Co-authored-by: Bruno <[email protected]>
2026-09-10 23:47:35 -04:00

73 lines
2.6 KiB
Python

"""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()