Build Forgejo-backed community prototype
This commit is contained in:
parent
797ae5ea35
commit
6671a01d26
16 changed files with 2485 additions and 293 deletions
|
|
@ -14,8 +14,9 @@ class ForgejoClientError(RuntimeError):
|
|||
|
||||
|
||||
class ForgejoClient:
|
||||
def __init__(self, settings: Settings) -> None:
|
||||
def __init__(self, settings: Settings, forgejo_token: str | None = None) -> None:
|
||||
self._settings = settings
|
||||
self._forgejo_token = forgejo_token or settings.forgejo_token
|
||||
self._client = httpx.AsyncClient(timeout=settings.forgejo_request_timeout_seconds)
|
||||
|
||||
async def __aenter__(self) -> ForgejoClient:
|
||||
|
|
@ -30,6 +31,54 @@ class ForgejoClient:
|
|||
absolute_url=True,
|
||||
)
|
||||
|
||||
async def exchange_oauth_code(
|
||||
self,
|
||||
*,
|
||||
token_endpoint: str,
|
||||
client_id: str,
|
||||
client_secret: str,
|
||||
code: str,
|
||||
redirect_uri: str,
|
||||
code_verifier: str,
|
||||
) -> dict[str, Any]:
|
||||
response = await self._client.post(
|
||||
token_endpoint,
|
||||
data={
|
||||
"grant_type": "authorization_code",
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"code": code,
|
||||
"redirect_uri": redirect_uri,
|
||||
"code_verifier": code_verifier,
|
||||
},
|
||||
headers={"Accept": "application/json"},
|
||||
)
|
||||
if response.is_success:
|
||||
payload = response.json()
|
||||
if isinstance(payload, dict):
|
||||
return payload
|
||||
raise ForgejoClientError("Unexpected Forgejo OAuth token payload.")
|
||||
|
||||
message = self._extract_error_message(response)
|
||||
raise ForgejoClientError(f"{response.status_code} from Forgejo OAuth: {message}")
|
||||
|
||||
async def fetch_userinfo(self, userinfo_endpoint: str, access_token: str) -> dict[str, Any]:
|
||||
response = await self._client.get(
|
||||
userinfo_endpoint,
|
||||
headers={
|
||||
"Accept": "application/json",
|
||||
"Authorization": f"Bearer {access_token}",
|
||||
},
|
||||
)
|
||||
if response.is_success:
|
||||
payload = response.json()
|
||||
if isinstance(payload, dict):
|
||||
return payload
|
||||
raise ForgejoClientError("Unexpected Forgejo UserInfo payload.")
|
||||
|
||||
message = self._extract_error_message(response)
|
||||
raise ForgejoClientError(f"{response.status_code} from Forgejo UserInfo: {message}")
|
||||
|
||||
async def fetch_current_user(self) -> dict[str, Any]:
|
||||
return await self._get_json("/api/v1/user", auth_required=True)
|
||||
|
||||
|
|
@ -39,22 +88,37 @@ class ForgejoClient:
|
|||
params={
|
||||
"limit": self._settings.forgejo_repo_scan_limit,
|
||||
"page": 1,
|
||||
"private": "false",
|
||||
"is_private": "false",
|
||||
},
|
||||
auth_required=True,
|
||||
)
|
||||
data = payload.get("data", [])
|
||||
return [repo for repo in data if isinstance(repo, dict)]
|
||||
|
||||
async def search_recent_issues(self) -> list[dict[str, Any]]:
|
||||
async def fetch_repository(self, owner: str, repo: str) -> dict[str, Any]:
|
||||
payload = await self._get_json(
|
||||
"/api/v1/repos/issues/search",
|
||||
f"/api/v1/repos/{owner}/{repo}",
|
||||
)
|
||||
if isinstance(payload, dict):
|
||||
return payload
|
||||
raise ForgejoClientError(f"Unexpected repository payload for {owner}/{repo}")
|
||||
|
||||
async def list_repo_issues(
|
||||
self,
|
||||
owner: str,
|
||||
repo: str,
|
||||
*,
|
||||
limit: int,
|
||||
) -> list[dict[str, Any]]:
|
||||
payload = await self._get_json(
|
||||
f"/api/v1/repos/{owner}/{repo}/issues",
|
||||
params={
|
||||
"state": "open",
|
||||
"page": 1,
|
||||
"limit": self._settings.forgejo_recent_issue_limit,
|
||||
"limit": limit,
|
||||
"type": "issues",
|
||||
"sort": "recentupdate",
|
||||
},
|
||||
auth_required=True,
|
||||
)
|
||||
if isinstance(payload, list):
|
||||
return [issue for issue in payload if isinstance(issue, dict)]
|
||||
|
|
@ -65,7 +129,7 @@ class ForgejoClient:
|
|||
if path:
|
||||
endpoint = f"{endpoint}/{path.strip('/')}"
|
||||
|
||||
payload = await self._get_json(endpoint, auth_required=True)
|
||||
payload = await self._get_json(endpoint)
|
||||
if isinstance(payload, list):
|
||||
return [entry for entry in payload if isinstance(entry, dict)]
|
||||
if isinstance(payload, dict):
|
||||
|
|
@ -80,16 +144,31 @@ class ForgejoClient:
|
|||
) -> list[dict[str, Any]]:
|
||||
payload = await self._get_json(
|
||||
f"/api/v1/repos/{owner}/{repo}/issues/{issue_number}/comments",
|
||||
auth_required=True,
|
||||
)
|
||||
if isinstance(payload, list):
|
||||
return [comment for comment in payload if isinstance(comment, dict)]
|
||||
return []
|
||||
|
||||
async def create_issue_comment(
|
||||
self,
|
||||
owner: str,
|
||||
repo: str,
|
||||
issue_number: int,
|
||||
body: str,
|
||||
) -> dict[str, Any]:
|
||||
payload = await self._request_json(
|
||||
"POST",
|
||||
f"/api/v1/repos/{owner}/{repo}/issues/{issue_number}/comments",
|
||||
json_payload={"body": body},
|
||||
auth_required=True,
|
||||
)
|
||||
if isinstance(payload, dict):
|
||||
return payload
|
||||
raise ForgejoClientError(f"Unexpected comment payload for {owner}/{repo}#{issue_number}")
|
||||
|
||||
async def get_file_content(self, owner: str, repo: str, path: str) -> dict[str, str]:
|
||||
payload = await self._get_json(
|
||||
f"/api/v1/repos/{owner}/{repo}/contents/{path.strip('/')}",
|
||||
auth_required=True,
|
||||
)
|
||||
if not isinstance(payload, dict):
|
||||
raise ForgejoClientError(f"Unexpected file payload for {owner}/{repo}:{path}")
|
||||
|
|
@ -118,17 +197,41 @@ class ForgejoClient:
|
|||
params: Mapping[str, str | int] | None = None,
|
||||
auth_required: bool = False,
|
||||
) -> dict[str, Any] | list[Any]:
|
||||
if auth_required and not self._settings.forgejo_token:
|
||||
return await self._request_json(
|
||||
"GET",
|
||||
path,
|
||||
absolute_url=absolute_url,
|
||||
params=params,
|
||||
auth_required=auth_required,
|
||||
)
|
||||
|
||||
async def _request_json(
|
||||
self,
|
||||
method: str,
|
||||
path: str,
|
||||
*,
|
||||
absolute_url: bool = False,
|
||||
params: Mapping[str, str | int] | None = None,
|
||||
json_payload: Mapping[str, object] | None = None,
|
||||
auth_required: bool = False,
|
||||
) -> dict[str, Any] | list[Any]:
|
||||
if auth_required and not self._forgejo_token:
|
||||
raise ForgejoClientError(
|
||||
"This Forgejo instance requires an authenticated API token for repo and issue access.",
|
||||
)
|
||||
|
||||
url = path if absolute_url else f"{self._settings.forgejo_base_url}{path}"
|
||||
headers = {}
|
||||
if self._settings.forgejo_token:
|
||||
headers["Authorization"] = f"token {self._settings.forgejo_token}"
|
||||
if self._forgejo_token:
|
||||
headers["Authorization"] = f"token {self._forgejo_token}"
|
||||
|
||||
response = await self._client.get(url, params=params, headers=headers)
|
||||
response = await self._client.request(
|
||||
method,
|
||||
url,
|
||||
params=params,
|
||||
headers=headers,
|
||||
json=json_payload,
|
||||
)
|
||||
if response.is_success:
|
||||
return response.json()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue