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:
@@ -1,5 +1,6 @@
|
|||||||
"""Git provider abstraction, credentials, SSH keys, and operations."""
|
"""Git provider abstraction, credentials, SSH keys, and operations."""
|
||||||
|
|
||||||
|
from app.git.connection import ConnectionManager, RepositoryConnectionData
|
||||||
from app.git.credentials import AccessTokenCredential, CredentialStorage, GitCredential
|
from app.git.credentials import AccessTokenCredential, CredentialStorage, GitCredential
|
||||||
from app.git.provider import GitProvider
|
from app.git.provider import GitProvider
|
||||||
from app.git.ssh_key import SshKeyLifecycle, SshKeyPair
|
from app.git.ssh_key import SshKeyLifecycle, SshKeyPair
|
||||||
@@ -7,12 +8,14 @@ from app.git.types import ConnectionStatus, CredentialKind, ProviderKind, SshKey
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"AccessTokenCredential",
|
"AccessTokenCredential",
|
||||||
|
"ConnectionManager",
|
||||||
"ConnectionStatus",
|
"ConnectionStatus",
|
||||||
"CredentialKind",
|
"CredentialKind",
|
||||||
"CredentialStorage",
|
"CredentialStorage",
|
||||||
"GitCredential",
|
"GitCredential",
|
||||||
"GitProvider",
|
"GitProvider",
|
||||||
"ProviderKind",
|
"ProviderKind",
|
||||||
|
"RepositoryConnectionData",
|
||||||
"SshKeyLifecycle",
|
"SshKeyLifecycle",
|
||||||
"SshKeyPair",
|
"SshKeyPair",
|
||||||
"SshKeyStatus",
|
"SshKeyStatus",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
@@ -3,6 +3,7 @@ from app.models.base import Base
|
|||||||
from app.models.config import Config
|
from app.models.config import Config
|
||||||
from app.models.project import Project
|
from app.models.project import Project
|
||||||
from app.models.repository import Repository
|
from app.models.repository import Repository
|
||||||
|
from app.models.repository_connection import RepositoryConnection
|
||||||
from app.models.secret import Secret
|
from app.models.secret import Secret
|
||||||
from app.models.tool_definition import ToolDefinition
|
from app.models.tool_definition import ToolDefinition
|
||||||
from app.models.tool_instance import ToolInstance
|
from app.models.tool_instance import ToolInstance
|
||||||
@@ -15,6 +16,7 @@ __all__ = [
|
|||||||
"Config",
|
"Config",
|
||||||
"Project",
|
"Project",
|
||||||
"Repository",
|
"Repository",
|
||||||
|
"RepositoryConnection",
|
||||||
"Secret",
|
"Secret",
|
||||||
"ToolDefinition",
|
"ToolDefinition",
|
||||||
"ToolInstance",
|
"ToolInstance",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from app.models.base import Base, TimestampMixin, UUIDMixin
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from app.models.project import Project
|
from app.models.project import Project
|
||||||
|
from app.models.repository_connection import RepositoryConnection
|
||||||
|
|
||||||
|
|
||||||
class Repository(Base, UUIDMixin, TimestampMixin):
|
class Repository(Base, UUIDMixin, TimestampMixin):
|
||||||
@@ -28,3 +29,6 @@ class Repository(Base, UUIDMixin, TimestampMixin):
|
|||||||
project: Mapped["Project"] = relationship(
|
project: Mapped["Project"] = relationship(
|
||||||
back_populates="repositories"
|
back_populates="repositories"
|
||||||
)
|
)
|
||||||
|
connections: Mapped[list["RepositoryConnection"]] = relationship(
|
||||||
|
back_populates="repository"
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"""RepositoryConnection links a project to a Git repository via a provider."""
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from sqlalchemy import ForeignKey, String
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from app.models.base import Base, TimestampMixin, UUIDMixin
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from app.models.repository import Repository
|
||||||
|
|
||||||
|
|
||||||
|
class RepositoryConnection(Base, UUIDMixin, TimestampMixin):
|
||||||
|
__tablename__ = "repository_connection"
|
||||||
|
|
||||||
|
project_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
ForeignKey("project.id"), index=True
|
||||||
|
)
|
||||||
|
repository_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
ForeignKey("repository.id"), nullable=True, index=True
|
||||||
|
)
|
||||||
|
provider_kind: Mapped[str] = mapped_column(
|
||||||
|
String(50), default="generic"
|
||||||
|
)
|
||||||
|
credential_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
index=True, nullable=True
|
||||||
|
)
|
||||||
|
connection_status: Mapped[str] = mapped_column(
|
||||||
|
String(50), default="pending"
|
||||||
|
)
|
||||||
|
default_branch: Mapped[str | None] = mapped_column(
|
||||||
|
String(100), nullable=True
|
||||||
|
)
|
||||||
|
|
||||||
|
repository: Mapped["Repository"] = relationship(
|
||||||
|
back_populates="connections"
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user