Architecture
This document describes the high-level architecture and design of
python-gitea: how the package is laid out, how the client and CLI relate, and
the design principles behind them.
Overview
python-gitea is a thin, typed wrapper around the
Gitea REST API. It provides two user-facing
surfaces that share the same domain model:
- Python client - synchronous (
Gitea) and asynchronous (AsyncGitea) clients with one resource object per Gitea domain. - CLI -
gitea-cli, a Typer application with one command group per domain.
Both surfaces authenticate to Gitea with a personal access token and base URL.
The CLI resolves configured accounts through cli/utils/auth.py
(Configuration), while Python callers pass
their credentials directly to the Gitea or AsyncGitea constructor.
Package Layout
src/gitea/
├── __init__.py # Top-level package (exports __version__)
├── __main__.py # python -m gitea
├── version.py # Version information
├── client/ # Client classes
│ ├── base.py # Client base class (token, base_url, URL building)
│ ├── gitea.py # Synchronous Gitea client
│ └── async_gitea.py # Asynchronous AsyncGitea client (aiohttp)
├── config/ # Configuration layer
│ ├── model.py # Pydantic models (AccountConfig, Config)
│ └── manager.py # ConfigManager (load/save/CRUD accounts)
├── resource/ # Resource base class
│ └── resource.py # Shared request helpers for resources
├── actions/ # Actions resource (sync + async), composed per family
│ ├── base.py # Every Actions path and query parameter
│ ├── scope.py # Which scope a set of coordinates addresses
│ ├── actions.py # Workflows, runs, jobs; composes the families below
│ ├── run_management.py # Cancel, approve, rerun, delete a run
│ ├── artifact.py # The files a run produced, archive included
│ ├── secret.py # Write-only secrets of a scope
│ ├── variable.py # Their readable counterpart
│ └── runner.py # Runners of a scope, and the registration token
├── issue/ # Issue resource (sync + async)
├── pull_request/ # Pull request resource (sync + async)
├── repository/ # Repository resource (sync + async)
├── user/ # User resource (sync + async)
├── comment/ # Comment resource (sync + async)
├── label/ # Label resource (sync + async)
├── milestone/ # Milestone resource (sync + async)
├── notification/ # Notification resource (sync + async)
├── project/ # Project resource (sync + async)
├── organization/ # Organization resource (sync + async)
├── cli/ # Typer CLI application
│ ├── main.py # gitea-cli entry point, command registration
│ ├── config/ # config commands
│ ├── actions/ # actions workflow, run, job, artifact, secret, variable, runner commands
│ ├── issue/ # issue + issue dependency commands
│ ├── pull_request/ # pull-request commands
│ ├── comment/ # comment commands
│ ├── label/ # label commands
│ ├── milestone/ # milestone commands
│ ├── notification/ # notification commands
│ ├── project/ # project, column, and project-issue commands
│ ├── organization/ # org commands
│ ├── repository/ # repo commands
│ ├── user/ # user commands
│ └── utils/ # auth resolution, API helpers, conversions
└── utils/ # Logging, field names, pagination, and response helpers
├── fields.py # Field-name convention and compatibility aliases
├── log.py # Logger setup
├── pagination.py # Paginated-listing walkers
└── response.py # Response processing helpers
Client Layer
The client layer is the programmatic interface. Both clients inherit from
Client, which stores the token and base URL and builds API URLs.
gitea.client.gitea.Giteausesrequestsand must be used as a context manager so its HTTP session is closed.gitea.client.async_gitea.AsyncGiteausesaiohttpand must be used as an async context manager.
Each client exposes one attribute per resource:
with Gitea(token="...", base_url="...") as client:
client.actions # Actions
client.issue # Issue
client.pull_request # PullRequest
client.repository # Repository
client.user # User
client.comment # Comment
client.label # Label
client.milestone # Milestone
client.notification # Notification
client.project # Project
client.organization # Organization
Resource Layer
Each resource module contains a synchronous class (e.g. gitea.issue.Issue) and
an asynchronous class (e.g. gitea.issue.AsyncIssue) with the same method
names. Methods map one-to-one onto Gitea REST endpoints and return either:
- a
(data, status)tuple for list/get operations, wheredatais the decoded JSON payload andstatuscarries metadata such as the HTTP status code, or - the raw HTTP response for mutating operations.
Work that spans several endpoints lives beside the resource rather than inside
it, so the resource methods stay one-to-one with the API. For example,
gitea.issue.project_column resolves the board column an issue's card sits in,
which Gitea only reveals through the project's column listings.
actions is the one resource split further, into a module per family - runs,
artifacts, secrets, variables, runners - which Actions and AsyncActions
compose. Gitea's Actions API is large enough that one class per client would be
a file nobody reads end to end, and the families differ from each other in ways
worth writing down beside their own methods: which listings answer with an
object and which with a bare array, which endpoints answer with a file rather
than a document, and which scopes each family exists at. The path building for
all of them stays in one base.py, so every URL the resource can address is in
one place.
That package's __init__.py re-exports nothing, unlike its neighbours'. With
thirteen modules an eager re-export would make import gitea.actions.scope
execute all of them, and would put each in an import cycle: importing a
submodule imports its package, so a package that imports its submodules is one
they import back. tests/actions/test_actions_imports.py pins both properties.
The neighbours keep their re-exports and avoid the same two costs by resolving
them on first read instead, through a module-level __getattr__ (PEP 562) that
gitea._lazy.lazy_reexports builds from a table of name against origin module.
from gitea.issue import Issue works exactly as it did, while
import gitea.issue.base no longer executes either client - and the package no
longer imports the submodules that import it back, which is where 44 of the
import cycles in this tree came from. Each such package keeps a TYPE_CHECKING
block naming the same imports, so editors and type checkers still resolve the
names statically; that block does not execute, so it reintroduces no cycle.
tests/test_lazy_reexports.py pins the public names, the laziness, and the
absence of any module-level import cycle anywhere in gitea.
CLI Layer
The CLI is a single Typer application registered in cli/main.py. Each resource
has a command group (e.g. gitea-cli issue, gitea-cli project), and nested
groups exist where the domain nests (e.g. project column, issue dependency).
CLI commands reuse the client through thin helper functions in cli/utils/
(auth.py resolves authentication, api.py and convert.py handle requests
and option conversion), so the CLI and the Python API stay in sync with the same
endpoint definitions.
Configuration Layer
The config/ module stores named accounts in a YAML file:
model.pydefines the Pydantic modelsAccountConfigandConfig.manager.pyprovidesConfigManagerfor loading, saving, and CRUD on accounts, including the default-account concept.
The CLI resolves authentication through cli/utils/auth.py, which implements
the precedence rules documented in
Configuration. Python callers instead pass
credentials directly to the client constructor; they can use ConfigManager to
load stored accounts programmatically.
Design Principles
- Thin wrappers, no business logic. The package mirrors the Gitea API surface instead of abstracting it, keeping behavior predictable.
- Synchronous and asynchronous parity. Sync and async classes expose the same methods, so switching between them only changes the client and awaits.
- One source of endpoint definitions. CLI and client share the request layer, avoiding drift between the two surfaces.
- Type hints and validation everywhere. Pydantic models for config, full type annotations on all public methods, and validated option types on the CLI.