|
import aiohttp
|
|
|
|
class GiteaClient:
|
|
"""
|
|
Asynchronous Gitea API client using aiohttp.
|
|
Uses a persistent ClientSession and base_url for all requests.
|
|
|
|
Args:
|
|
base_url (str): Root server endpoint (e.g. 'https://gitea.example.com').
|
|
token (str|None): API token sent in 'Authorization: token <token>'.
|
|
otp (str|None): Optional TOTP for X-Gitea-OTP header (for MFA-protected endpoints).
|
|
sudo (str|None): Optional user to impersonate via Sudo header.
|
|
timeout (int|float): Session-wide total request timeout seconds.
|
|
|
|
Attributes:
|
|
session (aiohttp.ClientSession): Created on start(), closed on stop().
|
|
"""
|
|
def __init__(self, base_url, token=None, otp=None, sudo=None, timeout=30):
|
|
self.base_url = base_url.rstrip("/")
|
|
self.token = token
|
|
self.otp = otp
|
|
self.sudo = sudo
|
|
self.timeout = aiohttp.ClientTimeout(total=timeout)
|
|
self.session = None
|
|
|
|
async def start(self):
|
|
"""
|
|
Open persistent aiohttp.ClientSession with preconfigured headers.
|
|
"""
|
|
headers = {"Accept": "application/json"}
|
|
if self.token:
|
|
headers["Authorization"] = f"token {self.token}"
|
|
if self.otp:
|
|
headers["X-Gitea-OTP"] = self.otp
|
|
if self.sudo:
|
|
headers["Sudo"] = self.sudo
|
|
self.session = aiohttp.ClientSession(headers=headers, timeout=self.timeout)
|
|
|
|
async def stop(self):
|
|
"""
|
|
Close persistent aiohttp.ClientSession.
|
|
"""
|
|
if self.session:
|
|
await self.session.close()
|
|
|
|
async def _request(self, method, path, params=None, json=None):
|
|
"""
|
|
Internal unified low-level API call executor.
|
|
|
|
Args:
|
|
method (str): HTTP method, e.g., 'GET', 'POST', 'PATCH', etc.
|
|
path (str): API path relative to base_url (e.g., 'api/v1/user').
|
|
params (dict|None): Query parameters.
|
|
json (dict|None): JSON-encoded request payload.
|
|
|
|
Returns:
|
|
dict:
|
|
status (int): HTTP response status code.
|
|
headers (dict): HTTP response headers.
|
|
data (dict|list|str): Parsed JSON response if Content-Type is application/json,
|
|
otherwise raw text.
|
|
"""
|
|
url = f"{self.base_url}/{path.lstrip('/')}"
|
|
async with self.session.request(method, url, params=params, json=json) as resp:
|
|
status = resp.status
|
|
headers = dict(resp.headers)
|
|
ct = headers.get("Content-Type", "")
|
|
if "application/json" in ct:
|
|
data = await resp.json()
|
|
else:
|
|
data = await resp.text()
|
|
return {"status": status, "headers": headers, "data": data}
|
|
|
|
async def me(self):
|
|
"""
|
|
Get the authenticated user's detailed account info.
|
|
|
|
Returns:
|
|
dict:
|
|
status (int): HTTP 200 if OK.
|
|
headers (dict): Response headers.
|
|
data (dict): JSON user object.
|
|
|
|
JSON Format Example:
|
|
{
|
|
"id": 123,
|
|
"login": "username",
|
|
"full_name": "User Name",
|
|
"email": "user@example.com",
|
|
...
|
|
}
|
|
"""
|
|
return await self._request("GET", "api/v1/user")
|
|
|
|
async def user(self, username):
|
|
"""
|
|
Get public profile of a given user.
|
|
|
|
Args:
|
|
username (str): Target username.
|
|
|
|
Returns:
|
|
dict:
|
|
status (int): HTTP 200 if OK; 404 if missing.
|
|
headers (dict): Response headers.
|
|
data (dict): JSON user object (public fields).
|
|
|
|
JSON Format:
|
|
{
|
|
"id": 124,
|
|
"login": "publicname",
|
|
"full_name": "",
|
|
...
|
|
}
|
|
"""
|
|
return await self._request("GET", f"api/v1/users/{username}")
|
|
|
|
async def user_tokens(self, username, page=None, limit=None):
|
|
"""
|
|
List user access tokens for admin or self.
|
|
|
|
Args:
|
|
username (str): Username to list tokens for.
|
|
page (int|None): Pagination.
|
|
limit (int|None): Page size.
|
|
|
|
Returns:
|
|
dict:
|
|
status (int): HTTP 200.
|
|
headers (dict): Contains X-Total-Count for pagination.
|
|
data (list): List of token meta objects (no plaintext token).
|
|
|
|
JSON Format Example:
|
|
[
|
|
{
|
|
"id": 873,
|
|
"name": "ci",
|
|
"sha1_last_eight": "abcd1234",
|
|
...
|
|
}
|
|
]
|
|
"""
|
|
params = {}
|
|
if page is not None:
|
|
params["page"] = page
|
|
if limit is not None:
|
|
params["limit"] = limit
|
|
return await self._request("GET", f"api/v1/users/{username}/tokens", params=params)
|
|
|
|
async def create_token(self, username, name):
|
|
"""
|
|
Create a new API token for a user (requires BasicAuth).
|
|
|
|
Args:
|
|
username (str): Username to create token for.
|
|
name (str): New token name.
|
|
|
|
Returns:
|
|
dict:
|
|
status (int): HTTP 201 if created.
|
|
headers (dict): Response headers.
|
|
data (dict): Token object including token sha1 seen only once.
|
|
|
|
JSON Format Example:
|
|
{
|
|
"id": 322,
|
|
"name": "ci-token",
|
|
"sha1": "tokenplaintextvalue",
|
|
"sha1_last_eight": "1234abcd"
|
|
}
|
|
"""
|
|
return await self._request("POST", f"api/v1/users/{username}/tokens", json={"name": name})
|
|
|
|
async def delete_token(self, username, token_id):
|
|
"""
|
|
Delete a token.
|
|
|
|
Args:
|
|
username (str): Username.
|
|
token_id (int): Token id.
|
|
|
|
Returns:
|
|
dict:
|
|
status (int): HTTP 204 on success.
|
|
headers (dict): Response headers.
|
|
data (str): '' (empty text).
|
|
|
|
JSON: No content (empty body) on success.
|
|
"""
|
|
return await self._request("DELETE", f"api/v1/users/{username}/tokens/{token_id}")
|
|
|
|
async def user_repos(self, username, page=None, limit=None, sort=None):
|
|
"""
|
|
List repositories for a user.
|
|
|
|
Args:
|
|
username (str): Username.
|
|
page (int|None): Page number.
|
|
limit (int|None): Per-page count.
|
|
sort (str|None): Sort field.
|
|
|
|
Returns:
|
|
dict:
|
|
status (int): HTTP 200.
|
|
headers (dict): Includes 'X-Total-Count' and pagination links.
|
|
data (list): List of repo objects.
|
|
|
|
JSON Format Example:
|
|
[
|
|
{
|
|
"id": 8,
|
|
"name": "repo1",
|
|
"full_name": "username/repo1",
|
|
"private": false,
|
|
"owner": {...},
|
|
...
|
|
},
|
|
...
|
|
]
|
|
"""
|
|
params = {}
|
|
if page is not None:
|
|
params["page"] = page
|
|
if limit is not None:
|
|
params["limit"] = limit
|
|
if sort is not None:
|
|
params["sort"] = sort
|
|
return await self._request("GET", f"api/v1/users/{username}/repos", params=params)
|
|
|
|
async def repo(self, owner, repo):
|
|
"""
|
|
Get full metadata for a repository.
|
|
|
|
Args:
|
|
owner (str): Username/Org.
|
|
repo (str): Repository name.
|
|
|
|
Returns:
|
|
dict:
|
|
status (int): HTTP 200 or 404.
|
|
headers (dict): Response headers.
|
|
data (dict): Repository object.
|
|
|
|
JSON Format Example:
|
|
{
|
|
"id": 8,
|
|
"name": "repo1",
|
|
"full_name": "user/repo1",
|
|
"private": false,
|
|
"description": "",
|
|
"forks_count": 0,
|
|
...
|
|
}
|
|
"""
|
|
return await self._request("GET", f"api/v1/repos/{owner}/{repo}")
|
|
|
|
async def create_repo(self, name, private=False, description=None, auto_init=False, default_branch=None):
|
|
"""
|
|
Create a repository for the authenticated user.
|
|
|
|
Args:
|
|
name (str): Repository name.
|
|
private (bool): Create as private repo.
|
|
description (str|None): Optional description.
|
|
auto_init (bool): Initialize with README.
|
|
default_branch (str|None): Set initial branch name.
|
|
|
|
Returns:
|
|
dict:
|
|
status (int): HTTP 201 if created.
|
|
headers (dict): Response headers.
|
|
data (dict): Repository object.
|
|
|
|
JSON Format: as per `repo()`
|
|
"""
|
|
payload = {"name": name, "private": private, "auto_init": auto_init}
|
|
if description is not None:
|
|
payload["description"] = description
|
|
if default_branch is not None:
|
|
payload["default_branch"] = default_branch
|
|
return await self._request("POST", "api/v1/user/repos", json=payload)
|
|
|
|
async def repo_issues(self, owner, repo, state=None, page=None, limit=None):
|
|
"""
|
|
List issues for a repository.
|
|
|
|
Args:
|
|
owner (str): Owner.
|
|
repo (str): Repo name.
|
|
state (str|None): Optional state ('open', 'closed', 'all').
|
|
page (int|None): Page.
|
|
limit (int|None): Per-page count.
|
|
|
|
Returns:
|
|
dict:
|
|
status (int): HTTP 200.
|
|
headers (dict): Pagination info.
|
|
data (list): List of issue objects.
|
|
|
|
JSON Example:
|
|
[
|
|
{
|
|
"id": 1,
|
|
"index": 2,
|
|
"title": "Bug found",
|
|
"state": "open",
|
|
"user": {...},
|
|
...
|
|
}, ...
|
|
]
|
|
"""
|
|
params = {}
|
|
if state is not None:
|
|
params["state"] = state
|
|
if page is not None:
|
|
params["page"] = page
|
|
if limit is not None:
|
|
params["limit"] = limit
|
|
return await self._request("GET", f"api/v1/repos/{owner}/{repo}/issues", params=params)
|
|
|
|
async def create_issue(self, owner, repo, title, body=None, assignees=None, labels=None, milestone=None):
|
|
"""
|
|
Create a new issue.
|
|
|
|
Args:
|
|
owner (str): Owner.
|
|
repo (str): Repo name.
|
|
title (str): Issue title.
|
|
body (str|None): Issue text.
|
|
assignees (list|None): Assignees.
|
|
labels (list|None): Labels.
|
|
milestone (int|None): Milestone number.
|
|
|
|
Returns:
|
|
dict:
|
|
status (int): HTTP 201 if created.
|
|
headers (dict): Response headers.
|
|
data (dict): Issue object.
|
|
|
|
JSON Format Example:
|
|
{
|
|
"id": 1,
|
|
"index": 2,
|
|
"title": "Bug",
|
|
"body": "...",
|
|
"state": "open",
|
|
...
|
|
}
|
|
"""
|
|
payload = {"title": title}
|
|
if body is not None:
|
|
payload["body"] = body
|
|
if assignees is not None:
|
|
payload["assignees"] = assignees
|
|
if labels is not None:
|
|
payload["labels"] = labels
|
|
if milestone is not None:
|
|
payload["milestone"] = milestone
|
|
return await self._request("POST", f"api/v1/repos/{owner}/{repo}/issues", json=payload)
|
|
|