feat(FN-011): complete Step 4 — repository connection model and connection manager

Fusion-Task-Id: FN-011
Fusion-Task-Lineage: 4a9aca6f-9d91-43aa-8d2a-d59657c1541a
This commit is contained in:
Fusion
2026-05-14 07:47:37 +02:00
parent edfbce9635
commit cffeb11410
5 changed files with 141 additions and 0 deletions
+93
View File
@@ -0,0 +1,93 @@
"""Repository connection orchestration."""
import uuid
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.git.credentials import CredentialStorage, GitCredential
from app.git.provider import GitProvider
from app.git.types import ConnectionStatus, ProviderKind
from app.models.repository_connection import RepositoryConnection
class RepositoryConnectionData(BaseModel):
"""Domain-level read model for a repository connection."""
id: uuid.UUID
project_id: uuid.UUID
repository_id: uuid.UUID | None
provider_kind: ProviderKind
credential_id: uuid.UUID | None
connection_status: ConnectionStatus
default_branch: str | None
class ConnectionManager:
"""Orchestrates creating, validating, and retrieving repository connections."""
def __init__(self, provider: GitProvider, storage: CredentialStorage) -> None:
self.provider = provider
self.storage = storage
async def connect(
self,
session: AsyncSession,
project_id: uuid.UUID,
git_url: str,
credential: GitCredential,
) -> RepositoryConnectionData:
"""Store *credential*, create a connection row, and validate with the provider."""
credential_id = self.storage.create(credential)
row = RepositoryConnection(
project_id=project_id,
provider_kind=str(self.provider.get_kind()),
credential_id=credential_id,
connection_status=str(ConnectionStatus.pending),
)
session.add(row)
await session.flush()
status = self.provider.validate_connection(git_url, str(credential_id))
if status == ConnectionStatus.connected:
row.connection_status = str(ConnectionStatus.connected)
else:
row.connection_status = str(ConnectionStatus.error)
await session.flush()
raise RuntimeError("Connection validation failed")
await session.refresh(row)
return _map_row(row)
async def disconnect(
self, session: AsyncSession, connection_id: uuid.UUID
) -> None:
"""Mark the connection as disconnected."""
row = await session.get(RepositoryConnection, connection_id)
if row is None:
return
row.connection_status = str(ConnectionStatus.disconnected)
await session.flush()
async def get_connection(
self, session: AsyncSession, connection_id: uuid.UUID
) -> RepositoryConnectionData | None:
"""Fetch a connection by ID and map it to the Pydantic read model."""
row = await session.get(RepositoryConnection, connection_id)
if row is None:
return None
return _map_row(row)
def _map_row(row: RepositoryConnection) -> RepositoryConnectionData:
return RepositoryConnectionData(
id=row.id,
project_id=row.project_id,
repository_id=row.repository_id,
provider_kind=ProviderKind(row.provider_kind),
credential_id=row.credential_id,
connection_status=ConnectionStatus(row.connection_status),
default_branch=row.default_branch,
)