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)