python-gitea 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
gitea/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """Top-level package for python-gitea."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from gitea.version import __version__
6
+
7
+ __all__ = ["__version__"]
gitea/__main__.py ADDED
@@ -0,0 +1,8 @@
1
+ """Main entry point for the python-gitea package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ if __name__ == "__main__":
6
+ from gitea.utils.log import setup_logger
7
+
8
+ setup_logger(print_version=True)
gitea/cli/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Command line interface for Gitea."""
gitea/cli/main.py ADDED
@@ -0,0 +1,128 @@
1
+ """Main entry point for the python-gitea CLI application."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import enum
6
+ from pathlib import Path
7
+ from typing import Annotated
8
+
9
+ import typer
10
+
11
+
12
+ class LoggingLevel(str, enum.Enum):
13
+ """Logging levels for the CLI."""
14
+
15
+ NOTSET = "NOTSET"
16
+ DEBUG = "DEBUG"
17
+ INFO = "INFO"
18
+ WARNING = "WARNING"
19
+ ERROR = "ERROR"
20
+ CRITICAL = "CRITICAL"
21
+
22
+
23
+ # Create the main Typer app
24
+ app = typer.Typer(
25
+ name="python-gitea",
26
+ help="Main CLI for python-gitea.",
27
+ rich_markup_mode="rich",
28
+ )
29
+
30
+
31
+ def setup_logging(level: LoggingLevel = LoggingLevel.INFO) -> None:
32
+ """Set up logging with Rich handler.
33
+
34
+ Args:
35
+ level: Logging level.
36
+
37
+ """
38
+ import logging # noqa: PLC0415
39
+
40
+ from rich.console import Console # noqa: PLC0415
41
+ from rich.logging import RichHandler # noqa: PLC0415
42
+
43
+ logger = logging.getLogger("python-gitea")
44
+
45
+ logger.setLevel(level.value)
46
+
47
+ console = Console()
48
+
49
+ # Remove any existing handlers to ensure RichHandler is used
50
+ for h in logger.handlers[:]: # Use slice copy to avoid modification during iteration
51
+ logger.removeHandler(h)
52
+ # Add the RichHandler
53
+
54
+ handler = RichHandler(
55
+ console=console,
56
+ rich_tracebacks=True,
57
+ show_time=True,
58
+ show_level=True, # Keep level (e.g., DEBUG, INFO) for clarity
59
+ markup=True, # Enable Rich markup in messages for styling
60
+ level=level.value, # Ensure handler respects the level
61
+ omit_repeated_times=False,
62
+ log_time_format="%H:%M",
63
+ )
64
+ handler.setLevel(level.value)
65
+ logger.addHandler(handler)
66
+
67
+ # Prevent propagation to root logger to avoid duplicate output
68
+ logger.propagate = False
69
+
70
+
71
+ @app.callback()
72
+ def main( # noqa: PLR0913
73
+ ctx: typer.Context,
74
+ output: Annotated[Path | None, typer.Option("--output", "-o", help="Output file name.")] = None,
75
+ token: Annotated[
76
+ str | None, typer.Option("--token", "-t", help="Gitea API token.", envvar="GITEA_API_TOKEN")
77
+ ] = None,
78
+ base_url: Annotated[
79
+ str,
80
+ typer.Option(
81
+ "--base-url",
82
+ "-b",
83
+ help="Base URL of the Gitea instance.",
84
+ envvar="GITEA_BASE_URL",
85
+ show_default=True,
86
+ ),
87
+ ] = "https://gitea.com",
88
+ timeout: Annotated[
89
+ int,
90
+ typer.Option(
91
+ "--timeout",
92
+ help="Timeout for API requests in seconds.",
93
+ show_default=True,
94
+ ),
95
+ ] = 30,
96
+ verbose: Annotated[
97
+ LoggingLevel,
98
+ typer.Option("--verbose", "-v", help="Set verbosity level."),
99
+ ] = LoggingLevel.INFO,
100
+ ) -> None:
101
+ """Enter the CLI application.
102
+
103
+ Args:
104
+ ctx: Typer context.
105
+ output: Output file name.
106
+ token: Gitea API token.
107
+ base_url: Base URL of the Gitea instance.
108
+ timeout: Timeout for API requests in seconds.
109
+ verbose: Verbosity level for logging.
110
+
111
+ """
112
+ setup_logging(verbose)
113
+ ctx.obj = {
114
+ "output": output,
115
+ "token": token,
116
+ "base_url": base_url,
117
+ "timeout": timeout,
118
+ }
119
+
120
+
121
+ def register_commands() -> None:
122
+ """Register CLI commands."""
123
+ from gitea.cli.user.main import user_app # noqa: PLC0415
124
+
125
+ app.add_typer(user_app, name="user", help="Commands for managing Gitea users.")
126
+
127
+
128
+ register_commands()
@@ -0,0 +1 @@
1
+ """User module for CLI commands."""
@@ -0,0 +1,40 @@
1
+ """Delete user-level runner command for Gitea CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+
10
+ def delete_user_level_runner_command(
11
+ ctx: typer.Context,
12
+ runner_id: Annotated[str, typer.Option("--runner-id", help="The ID of the runner to delete.")],
13
+ ) -> None:
14
+ """Delete a user-level runner for the authenticated user.
15
+
16
+ Args:
17
+ ctx: The Typer context.
18
+ runner_id: The ID of the runner to delete.
19
+
20
+ """
21
+ from typing import Any # noqa: PLC0415
22
+
23
+ import gitea.cli.utils # noqa: PLC0415
24
+ import gitea.client.gitea # noqa: PLC0415
25
+
26
+ token: str | None = ctx.obj.get("token")
27
+ base_url: str = ctx.obj.get("base_url")
28
+ timeout: int = ctx.obj.get("timeout")
29
+
30
+ def api_call() -> dict[str, Any] | None:
31
+ """Delete a user-level runner.
32
+
33
+ Returns:
34
+ A dictionary containing the result of the deletion.
35
+
36
+ """
37
+ with gitea.client.gitea.Gitea(token=token, base_url=base_url) as client:
38
+ return client.user.delete_user_level_runner(runner_id=runner_id, timeout=timeout)
39
+
40
+ gitea.cli.utils.execute_api_command(ctx=ctx, api_call=api_call, command_name="delete-user-level-runner")
@@ -0,0 +1,31 @@
1
+ """Get registration token command for Gitea CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+
8
+ def get_registration_token_command(
9
+ ctx: typer.Context,
10
+ ) -> None:
11
+ """Get registration token for the authenticated user."""
12
+ from typing import Any # noqa: PLC0415
13
+
14
+ import gitea.cli.utils # noqa: PLC0415
15
+ import gitea.client.gitea # noqa: PLC0415
16
+
17
+ token: str | None = ctx.obj.get("token")
18
+ base_url: str = ctx.obj.get("base_url")
19
+ timeout: int = ctx.obj.get("timeout")
20
+
21
+ def api_call() -> dict[str, Any] | None:
22
+ """Get registration token.
23
+
24
+ Returns:
25
+ A dictionary containing the registration token.
26
+
27
+ """
28
+ with gitea.client.gitea.Gitea(token=token, base_url=base_url) as client:
29
+ return client.user.get_registration_token(timeout=timeout)
30
+
31
+ gitea.cli.utils.execute_api_command(ctx=ctx, api_call=api_call, command_name="get-registration-token")
@@ -0,0 +1,42 @@
1
+ """Get user information command for Gitea CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+
10
+ def get_user_command(
11
+ ctx: typer.Context,
12
+ username: Annotated[
13
+ str | None, "The username of the user to retrieve. If None, retrieves the authenticated user."
14
+ ] = None,
15
+ ) -> None:
16
+ """Get user information.
17
+
18
+ Args:
19
+ ctx: The Typer context.
20
+ username: The username of the user to retrieve. If None, retrieves the authenticated user.
21
+
22
+ """
23
+ from typing import Any # noqa: PLC0415
24
+
25
+ import gitea.cli.utils # noqa: PLC0415
26
+ import gitea.client.gitea # noqa: PLC0415
27
+
28
+ token: str | None = ctx.obj.get("token")
29
+ base_url: str = ctx.obj.get("base_url")
30
+ timeout: int = ctx.obj.get("timeout")
31
+
32
+ def api_call() -> dict[str, Any] | None:
33
+ """Get user information.
34
+
35
+ Returns:
36
+ The user information as a dictionary.
37
+
38
+ """
39
+ with gitea.client.gitea.Gitea(token=token, base_url=base_url) as client:
40
+ return client.user.get_user(username=username, timeout=timeout)
41
+
42
+ gitea.cli.utils.execute_api_command(ctx=ctx, api_call=api_call, command_name="get-user")
@@ -0,0 +1,34 @@
1
+ """Get user-level runners command for Gitea CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+
10
+ def get_user_level_runners_command(
11
+ ctx: typer.Context,
12
+ runner_id: Annotated[str | None, typer.Option("--runner-id", help="The ID of the runner to retrieve.")] = None,
13
+ ) -> None:
14
+ """Get user-level runners for the authenticated user."""
15
+ from typing import Any # noqa: PLC0415
16
+
17
+ import gitea.cli.utils # noqa: PLC0415
18
+ import gitea.client.gitea # noqa: PLC0415
19
+
20
+ token: str | None = ctx.obj.get("token")
21
+ base_url: str = ctx.obj.get("base_url")
22
+ timeout: int = ctx.obj.get("timeout")
23
+
24
+ def api_call() -> dict[str, Any] | None:
25
+ """Get user-level runners.
26
+
27
+ Returns:
28
+ A dictionary containing the user-level runners.
29
+
30
+ """
31
+ with gitea.client.gitea.Gitea(token=token, base_url=base_url) as client:
32
+ return client.user.get_user_level_runners(runner_id=runner_id, timeout=timeout)
33
+
34
+ gitea.cli.utils.execute_api_command(ctx=ctx, api_call=api_call, command_name="get-user-level-runners")
@@ -0,0 +1,50 @@
1
+ """Get workflow jobs command for Gitea CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated, Literal
6
+
7
+ import typer
8
+
9
+
10
+ def get_workflow_jobs_command(
11
+ ctx: typer.Context,
12
+ status: Annotated[
13
+ Literal["pending", "queued", "in_progress", "failure", "success", "skipped"],
14
+ typer.Option(
15
+ "--status",
16
+ help="The status to filter workflow jobs by. Options: pending, queued, in_progress, failure, success, skipped.",
17
+ ),
18
+ ],
19
+ page: Annotated[int | None, typer.Option("--page", help="The page number for pagination.")] = None,
20
+ limit: Annotated[int | None, typer.Option("--limit", help="The number of items per page for pagination.")] = None,
21
+ ) -> None:
22
+ """Get workflow jobs for the authenticated user filtered by status.
23
+
24
+ Args:
25
+ ctx: The Typer context.
26
+ status: The status to filter workflow jobs by. Options: pending, queued, in_progress, failure, success, skipped.
27
+ page: The page number for pagination.
28
+ limit: The number of items per page for pagination.
29
+
30
+ """
31
+ from typing import Any # noqa: PLC0415
32
+
33
+ import gitea.cli.utils # noqa: PLC0415
34
+ import gitea.client.gitea # noqa: PLC0415
35
+
36
+ token: str | None = ctx.obj.get("token")
37
+ base_url: str = ctx.obj.get("base_url")
38
+ timeout: int = ctx.obj.get("timeout")
39
+
40
+ def api_call() -> dict[str, Any] | None:
41
+ """Get workflow jobs.
42
+
43
+ Returns:
44
+ A dictionary containing the workflow jobs with the specified status.
45
+
46
+ """
47
+ with gitea.client.gitea.Gitea(token=token, base_url=base_url) as client:
48
+ return client.user.get_workflow_jobs(status=status, page=page, limit=limit, timeout=timeout)
49
+
50
+ gitea.cli.utils.execute_api_command(ctx=ctx, api_call=api_call, command_name="get-workflow-jobs")
@@ -0,0 +1,67 @@
1
+ """Get workflow runs command for Gitea CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated, Literal
6
+
7
+ import typer
8
+
9
+
10
+ def get_workflow_runs_command( # noqa: PLR0913
11
+ ctx: typer.Context,
12
+ event: Annotated[str | None, typer.Option("--event", help="Workflow event name.")] = None,
13
+ branch: Annotated[str | None, typer.Option("--branch", help="Workflow branch.")] = None,
14
+ status: Annotated[
15
+ Literal["pending", "queued", "in_progress", "failure", "success", "skipped"] | None,
16
+ typer.Option(
17
+ "--status",
18
+ help="Workflow status.",
19
+ ),
20
+ ] = None,
21
+ actor: Annotated[str | None, typer.Option("--actor", help="Triggered by user.")] = None,
22
+ head_sha: Annotated[str | None, typer.Option("--head-sha", help="Triggering sha of the workflow run.")] = None,
23
+ page: Annotated[int | None, typer.Option("--page", help="Page number of results to return.")] = None,
24
+ limit: Annotated[int | None, typer.Option("--limit", help="Page size of results.")] = None,
25
+ ) -> None:
26
+ """Get workflow runs for the authenticated user filtered by various parameters.
27
+
28
+ Args:
29
+ ctx: The Typer context.
30
+ event: The event that triggered the workflow run.
31
+ branch: The branch name to filter workflow runs.
32
+ status: The status to filter workflow jobs by. Options: pending, queued, in_progress, failure, success, skipped.
33
+ actor: The actor who triggered the workflow run.
34
+ head_sha: The head SHA to filter workflow runs.
35
+ page: The page number for pagination.
36
+ limit: The number of items per page for pagination.
37
+
38
+ """
39
+ from typing import Any # noqa: PLC0415
40
+
41
+ import gitea.cli.utils # noqa: PLC0415
42
+ import gitea.client.gitea # noqa: PLC0415
43
+
44
+ token: str | None = ctx.obj.get("token")
45
+ base_url: str = ctx.obj.get("base_url")
46
+ timeout: int = ctx.obj.get("timeout")
47
+
48
+ def api_call() -> dict[str, Any] | None:
49
+ """Get workflow runs.
50
+
51
+ Returns:
52
+ A dictionary containing the workflow runs.
53
+
54
+ """
55
+ with gitea.client.gitea.Gitea(token=token, base_url=base_url) as client:
56
+ return client.user.get_workflow_runs(
57
+ event=event,
58
+ branch=branch,
59
+ status=status,
60
+ actor=actor,
61
+ head_sha=head_sha,
62
+ page=page,
63
+ limit=limit,
64
+ timeout=timeout,
65
+ )
66
+
67
+ gitea.cli.utils.execute_api_command(ctx=ctx, api_call=api_call, command_name="get-workflow-runs")
gitea/cli/user/main.py ADDED
@@ -0,0 +1,34 @@
1
+ # ruff: noqa PLC0415
2
+
3
+ """CLI commands for managing Gitea users."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import typer
8
+
9
+ user_app = typer.Typer(
10
+ name="user",
11
+ help="Commands for managing Gitea users.",
12
+ rich_markup_mode="rich",
13
+ )
14
+
15
+
16
+ def register_commands() -> None:
17
+ """Register user-related commands to the user_app."""
18
+
19
+ from gitea.cli.user.get_user import get_user_command
20
+ from gitea.cli.user.get_workflow_jobs import get_workflow_jobs_command
21
+ from gitea.cli.user.get_user_level_runners import get_user_level_runners_command
22
+ from gitea.cli.user.get_registration_token import get_registration_token_command
23
+ from gitea.cli.user.delete_user_level_runner import delete_user_level_runner_command
24
+ from gitea.cli.user.get_workflow_runs import get_workflow_runs_command
25
+
26
+ user_app.command("get-user")(get_user_command)
27
+ user_app.command("get-workflow-jobs")(get_workflow_jobs_command)
28
+ user_app.command("get-user-level-runners")(get_user_level_runners_command)
29
+ user_app.command("get-registration-token")(get_registration_token_command)
30
+ user_app.command("delete-user-level-runner")(delete_user_level_runner_command)
31
+ user_app.command("get-workflow-runs")(get_workflow_runs_command)
32
+
33
+
34
+ register_commands()
gitea/cli/utils.py ADDED
@@ -0,0 +1,46 @@
1
+ """CLI utility functions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections.abc import Callable
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import typer
11
+ from rich.console import Console
12
+
13
+
14
+ def execute_api_command(
15
+ ctx: typer.Context,
16
+ api_call: Callable[[], dict[str, Any] | None],
17
+ command_name: str = "Command",
18
+ ) -> None:
19
+ """Execute an API command and output results.
20
+
21
+ Args:
22
+ ctx: Typer context containing token, base_url, and output.
23
+ api_call: Callable that executes the API call and returns the result.
24
+ command_name: Name of the command for error messages.
25
+
26
+ """
27
+ output: Path | None = ctx.obj.get("output")
28
+ console = Console()
29
+
30
+ try:
31
+ result = api_call()
32
+
33
+ if result is None:
34
+ console.print(f"{command_name} executed successfully. No content returned.")
35
+ return
36
+
37
+ json_output = json.dumps(result, indent=4)
38
+
39
+ if output:
40
+ Path(output).write_text(json_output)
41
+ console.print(f"Output saved to {output}")
42
+ else:
43
+ console.print_json(json_output)
44
+ except Exception as e:
45
+ console.print(f"Error executing {command_name}: {e}", style="red")
46
+ raise typer.Exit(1) from e
@@ -0,0 +1,8 @@
1
+ """Client for Gitea API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from gitea.client.async_gitea import AsyncGitea
6
+ from gitea.client.gitea import Gitea
7
+
8
+ __all__ = ["AsyncGitea", "Gitea"]
@@ -0,0 +1,106 @@
1
+ """Asynchronous Gitea API client implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from aiohttp import ClientResponse, ClientSession, ClientTimeout
8
+
9
+ from gitea.client.base import Client
10
+
11
+
12
+ class AsyncGitea(Client): # pylint: disable=too-few-public-methods
13
+ """Asynchronous Gitea API client."""
14
+
15
+ def __init__(self, token: str | None = None, base_url: str = "https://gitea.com") -> None:
16
+ """Initialize the asynchronous Gitea client.
17
+
18
+ Args:
19
+ token: The API token for authentication.
20
+ base_url: The base URL of the Gitea instance.
21
+
22
+ """
23
+ super().__init__(token=token, base_url=base_url)
24
+ self.session: ClientSession | None = None
25
+
26
+ def __str__(self) -> str:
27
+ """Return a string representation of the AsyncGitea client.
28
+
29
+ Returns:
30
+ A string representing the AsyncGitea client.
31
+
32
+ """
33
+ return f"AsyncGitea Client(base_url={self.base_url})"
34
+
35
+ async def __aenter__(self) -> AsyncGitea:
36
+ """Enter the asynchronous context manager.
37
+
38
+ Returns:
39
+ The AsyncGitea client instance.
40
+
41
+ """
42
+ if self.session is not None and not self.session.closed:
43
+ raise RuntimeError("AsyncGitea session already open; do not re-enter context manager.")
44
+ self.session = ClientSession(headers=self.headers)
45
+ return self
46
+
47
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
48
+ """Exit the asynchronous context manager.
49
+
50
+ Args:
51
+ exc_type: The exception type.
52
+ exc_val: The exception value.
53
+ exc_tb: The traceback.
54
+
55
+ """
56
+ if self.session:
57
+ await self.session.close()
58
+ self.session = None
59
+
60
+ def _get_session(self, headers: dict | None = None, **kwargs: Any) -> ClientSession:
61
+ """Get or create the aiohttp ClientSession.
62
+
63
+ Args:
64
+ headers: Optional headers to include in the session.
65
+ **kwargs: Additional arguments for ClientSession.
66
+
67
+ Returns:
68
+ The aiohttp ClientSession instance.
69
+
70
+ """
71
+ return ClientSession(headers=headers, **kwargs)
72
+
73
+ async def _request(
74
+ self, method: str, endpoint: str, headers: dict | None = None, timeout: int = 30, **kwargs: Any
75
+ ) -> ClientResponse:
76
+ """Make an asynchronous HTTP request to the Gitea API.
77
+
78
+ Args:
79
+ method: The HTTP method (GET, POST, etc.).
80
+ endpoint: The API endpoint.
81
+ headers: Optional headers to include in the request.
82
+ timeout: Request timeout in seconds.
83
+ **kwargs: Additional arguments for the request.
84
+
85
+ Returns:
86
+ The aiohttp ClientResponse object.
87
+
88
+ """
89
+ if self.session is None:
90
+ raise RuntimeError(
91
+ "AsyncGitea must be used as an async context manager. "
92
+ + "Use 'async with AsyncGitea(...) as client:' to ensure proper resource cleanup."
93
+ )
94
+
95
+ url = self._build_url(endpoint=endpoint)
96
+ request_headers = {**self.headers, **(headers or {})}
97
+ timeout_obj = ClientTimeout(total=timeout)
98
+ response = await self.session.request(
99
+ method=method, url=url, headers=request_headers, timeout=timeout_obj, **kwargs
100
+ )
101
+ try:
102
+ response.raise_for_status()
103
+ except Exception:
104
+ response.release()
105
+ raise
106
+ return response
gitea/client/base.py ADDED
@@ -0,0 +1,45 @@
1
+ """Base client class for Gitea API interactions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ class Client: # pylint: disable=too-few-public-methods
9
+ """Abstract base class for Gitea clients."""
10
+
11
+ def __init__(self, token: str | None, base_url: str) -> None:
12
+ """Construct the base client.
13
+
14
+ Args:
15
+ token: The API token for authentication.
16
+ base_url: The base URL of the Gitea instance.
17
+
18
+ """
19
+ self.token = token
20
+ self.base_url = base_url.rstrip("/")
21
+ self.headers: dict[str, Any] = {}
22
+ if self.token:
23
+ self.headers["Authorization"] = f"token {self.token}"
24
+
25
+ @property
26
+ def api_url(self) -> str:
27
+ """Return the base API URL.
28
+
29
+ Returns:
30
+ str: The base API URL.
31
+
32
+ """
33
+ return f"{self.base_url}/api/v1"
34
+
35
+ def _build_url(self, endpoint: str) -> str:
36
+ """Construct the full URL for a given endpoint.
37
+
38
+ Args:
39
+ endpoint (str): The API endpoint.
40
+
41
+ Returns:
42
+ str: The full URL.
43
+
44
+ """
45
+ return f"{self.api_url}/{endpoint.lstrip('/')}"