feat(integrations): add base classes and exceptions

This commit is contained in:
2025-03-29 14:05:24 +00:00
parent 6f4bd36b8b
commit 9ec563a8db
3 changed files with 172 additions and 0 deletions

View File

@@ -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",
]

View File

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

View File

@@ -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