feat(integrations): add base classes and exceptions
This commit is contained in:
33
src/arbiter/integrations/__init__.py
Normal file
33
src/arbiter/integrations/__init__.py
Normal 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",
|
||||
]
|
||||
92
src/arbiter/integrations/base.py
Normal file
92
src/arbiter/integrations/base.py
Normal 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()
|
||||
47
src/arbiter/integrations/exceptions.py
Normal file
47
src/arbiter/integrations/exceptions.py
Normal 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
|
||||
Reference in New Issue
Block a user