From 9ec563a8dbfbda5f3510700f2a0b80d391b281de Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Sat, 29 Mar 2025 14:05:24 +0000 Subject: [PATCH] feat(integrations): add base classes and exceptions --- src/arbiter/integrations/__init__.py | 33 +++++++++ src/arbiter/integrations/base.py | 92 ++++++++++++++++++++++++++ src/arbiter/integrations/exceptions.py | 47 +++++++++++++ 3 files changed, 172 insertions(+) create mode 100644 src/arbiter/integrations/__init__.py create mode 100644 src/arbiter/integrations/base.py create mode 100644 src/arbiter/integrations/exceptions.py diff --git a/src/arbiter/integrations/__init__.py b/src/arbiter/integrations/__init__.py new file mode 100644 index 0000000..5c7c2ec --- /dev/null +++ b/src/arbiter/integrations/__init__.py @@ -0,0 +1,33 @@ +"""Platform integration clients for GitHub and GitLab.""" + +from arbiter.integrations.base import ( + CommitStatus, + Platform, + PlatformClient, + PullRequestInfo, +) +from arbiter.integrations.exceptions import ( + AuthenticationError, + IntegrationError, + NotFoundError, + PlatformError, + RateLimitError, +) +from arbiter.integrations.formatters import ReviewCommentFormatter +from arbiter.integrations.github import GitHubClient +from arbiter.integrations.gitlab import GitLabClient + +__all__ = [ + "AuthenticationError", + "CommitStatus", + "GitHubClient", + "GitLabClient", + "IntegrationError", + "NotFoundError", + "Platform", + "PlatformClient", + "PlatformError", + "PullRequestInfo", + "RateLimitError", + "ReviewCommentFormatter", +] diff --git a/src/arbiter/integrations/base.py b/src/arbiter/integrations/base.py new file mode 100644 index 0000000..e164de1 --- /dev/null +++ b/src/arbiter/integrations/base.py @@ -0,0 +1,92 @@ +"""Base classes and types for platform integrations.""" + +from abc import ABC, abstractmethod +from datetime import datetime +from enum import StrEnum + +from pydantic import BaseModel, Field + + +class Platform(StrEnum): + """Supported code hosting platforms.""" + + GITHUB = "github" + GITLAB = "gitlab" + + +class CommitStatus(StrEnum): + """Commit status states for PR checks.""" + + PENDING = "pending" + SUCCESS = "success" + FAILURE = "failure" + ERROR = "error" + + +class PullRequestInfo(BaseModel): + """Information about a pull request or merge request.""" + + platform: Platform = Field(description="Source platform") + repository: str = Field(description="Repository name (owner/repo format)") + pr_number: int = Field(description="PR/MR number") + head_sha: str = Field(description="Head commit SHA") + base_sha: str = Field(description="Base commit SHA") + head_ref: str = Field(description="Head branch name") + base_ref: str = Field(description="Base branch name") + title: str = Field(description="PR/MR title") + author: str | None = Field(default=None, description="PR/MR author username") + url: str = Field(description="Web URL to the PR/MR") + is_draft: bool = Field(default=False, description="Whether PR/MR is a draft") + + +class Comment(BaseModel): + """A comment on a pull request or merge request.""" + + id: str = Field(description="Comment ID on the platform") + body: str = Field(description="Comment body text") + author: str = Field(description="Comment author username") + url: str = Field(description="Web URL to the comment") + created_at: datetime = Field(description="When the comment was created") + + +class PlatformClient(ABC): + @property + @abstractmethod + def platform(self) -> Platform: ... + + @abstractmethod + async def get_pr_diff(self, repository: str, pr_number: int) -> str: ... + + @abstractmethod + async def post_comment(self, repository: str, pr_number: int, body: str) -> str: ... + + @abstractmethod + async def update_commit_status( + self, + repository: str, + sha: str, + status: CommitStatus, + description: str, + context: str, + target_url: str | None = None, + ) -> None: ... + + @abstractmethod + async def get_pr_info(self, repository: str, pr_number: int) -> PullRequestInfo: ... + + @abstractmethod + async def get_comments(self, repository: str, pr_number: int) -> list[Comment]: ... + + @abstractmethod + async def update_comment( + self, repository: str, pr_number: int, comment_id: str, body: str + ) -> str: ... + + @abstractmethod + async def close(self) -> None: ... + + async def __aenter__(self) -> "PlatformClient": + return self + + async def __aexit__(self, *args: object) -> None: + await self.close() diff --git a/src/arbiter/integrations/exceptions.py b/src/arbiter/integrations/exceptions.py new file mode 100644 index 0000000..50b691d --- /dev/null +++ b/src/arbiter/integrations/exceptions.py @@ -0,0 +1,47 @@ +"""Exception types for platform integrations.""" + + +class IntegrationError(Exception): + """Base exception for all platform integration errors.""" + + def __init__( + self, + message: str, + status_code: int | None = None, + response_body: str | None = None, + ) -> None: + super().__init__(message) + self.status_code = status_code + self.response_body = response_body + + +class AuthenticationError(IntegrationError): + """Raised on 401 Unauthorized or 403 Forbidden responses.""" + + pass + + +class RateLimitError(IntegrationError): + """Raised on 429 Too Many Requests responses.""" + + def __init__( + self, + message: str, + retry_after: int | None = None, + status_code: int | None = 429, + response_body: str | None = None, + ) -> None: + super().__init__(message, status_code, response_body) + self.retry_after = retry_after + + +class NotFoundError(IntegrationError): + """Raised on 404 Not Found responses.""" + + pass + + +class PlatformError(IntegrationError): + """Raised on 5xx server errors.""" + + pass