qodev-gitlab-api 0.1.0__tar.gz
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.
- qodev_gitlab_api-0.1.0/.github/workflows/ci.yml +23 -0
- qodev_gitlab_api-0.1.0/.github/workflows/publish.yml +16 -0
- qodev_gitlab_api-0.1.0/.gitignore +11 -0
- qodev_gitlab_api-0.1.0/PKG-INFO +45 -0
- qodev_gitlab_api-0.1.0/README.md +17 -0
- qodev_gitlab_api-0.1.0/pyproject.toml +84 -0
- qodev_gitlab_api-0.1.0/src/qodev_gitlab_api/__init__.py +18 -0
- qodev_gitlab_api-0.1.0/src/qodev_gitlab_api/_base.py +143 -0
- qodev_gitlab_api-0.1.0/src/qodev_gitlab_api/_files.py +64 -0
- qodev_gitlab_api-0.1.0/src/qodev_gitlab_api/_issues.py +119 -0
- qodev_gitlab_api-0.1.0/src/qodev_gitlab_api/_merge_requests.py +258 -0
- qodev_gitlab_api-0.1.0/src/qodev_gitlab_api/_pipelines.py +168 -0
- qodev_gitlab_api-0.1.0/src/qodev_gitlab_api/_releases.py +97 -0
- qodev_gitlab_api-0.1.0/src/qodev_gitlab_api/_variables.py +144 -0
- qodev_gitlab_api-0.1.0/src/qodev_gitlab_api/client.py +29 -0
- qodev_gitlab_api-0.1.0/src/qodev_gitlab_api/exceptions.py +30 -0
- qodev_gitlab_api-0.1.0/src/qodev_gitlab_api/models.py +34 -0
- qodev_gitlab_api-0.1.0/tests/conftest.py +54 -0
- qodev_gitlab_api-0.1.0/tests/test_client.py +400 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
lint:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: astral-sh/setup-uv@v4
|
|
13
|
+
- run: uv sync --all-extras
|
|
14
|
+
- run: uv run ruff check .
|
|
15
|
+
- run: uv run ruff format --check .
|
|
16
|
+
|
|
17
|
+
test:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- uses: astral-sh/setup-uv@v4
|
|
22
|
+
- run: uv sync --all-extras
|
|
23
|
+
- run: uv run pytest -v
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
tags: ["v*"]
|
|
5
|
+
|
|
6
|
+
jobs:
|
|
7
|
+
publish:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
environment: pypi
|
|
10
|
+
permissions:
|
|
11
|
+
id-token: write
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: astral-sh/setup-uv@v4
|
|
15
|
+
- run: uv build
|
|
16
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qodev-gitlab-api
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the GitLab API
|
|
5
|
+
Project-URL: Repository, https://github.com/qodevai/gitlab-api
|
|
6
|
+
Project-URL: Issues, https://github.com/qodevai/gitlab-api/issues
|
|
7
|
+
Author-email: Jan Scheffler <jan.scheffler@qodev.ai>
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: api,client,gitlab
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Requires-Dist: httpx>=0.28.1
|
|
20
|
+
Requires-Dist: python-dotenv>=1.1.0
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pyright>=1.1.0; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest-mock>=3.12.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: ruff>=0.8.0; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# qodev-gitlab-api
|
|
30
|
+
|
|
31
|
+
Python client for the GitLab API.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install qodev-gitlab-api
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from qodev_gitlab_api import GitLabClient
|
|
43
|
+
|
|
44
|
+
client = GitLabClient()
|
|
45
|
+
```
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "qodev-gitlab-api"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Python client for the GitLab API"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Jan Scheffler", email = "jan.scheffler@qodev.ai" }
|
|
8
|
+
]
|
|
9
|
+
license = { text = "MIT" }
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
keywords = ["gitlab", "api", "client"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 4 - Beta",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Typing :: Typed",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"httpx>=0.28.1",
|
|
24
|
+
"python-dotenv>=1.1.0",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Repository = "https://github.com/qodevai/gitlab-api"
|
|
29
|
+
Issues = "https://github.com/qodevai/gitlab-api/issues"
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = [
|
|
33
|
+
"pytest>=8.0.0",
|
|
34
|
+
"pytest-cov>=4.1.0",
|
|
35
|
+
"pytest-mock>=3.12.0",
|
|
36
|
+
"ruff>=0.8.0",
|
|
37
|
+
"pyright>=1.1.0",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[build-system]
|
|
41
|
+
requires = ["hatchling"]
|
|
42
|
+
build-backend = "hatchling.build"
|
|
43
|
+
|
|
44
|
+
[tool.hatch.build.targets.wheel]
|
|
45
|
+
packages = ["src/qodev_gitlab_api"]
|
|
46
|
+
|
|
47
|
+
[tool.ruff]
|
|
48
|
+
target-version = "py311"
|
|
49
|
+
line-length = 120
|
|
50
|
+
|
|
51
|
+
[tool.ruff.lint]
|
|
52
|
+
select = [
|
|
53
|
+
"E", # pycodestyle errors
|
|
54
|
+
"F", # pyflakes
|
|
55
|
+
"I", # isort
|
|
56
|
+
"UP", # pyupgrade
|
|
57
|
+
"B", # flake8-bugbear
|
|
58
|
+
"SIM", # flake8-simplify
|
|
59
|
+
"RUF", # ruff-specific rules
|
|
60
|
+
]
|
|
61
|
+
ignore = [
|
|
62
|
+
"E501", # line too long (handled by formatter)
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
[tool.ruff.lint.isort]
|
|
66
|
+
known-first-party = ["qodev_gitlab_api"]
|
|
67
|
+
|
|
68
|
+
[tool.ruff.format]
|
|
69
|
+
quote-style = "double"
|
|
70
|
+
indent-style = "space"
|
|
71
|
+
|
|
72
|
+
[tool.pyright]
|
|
73
|
+
pythonVersion = "3.11"
|
|
74
|
+
typeCheckingMode = "basic"
|
|
75
|
+
|
|
76
|
+
[tool.pytest.ini_options]
|
|
77
|
+
testpaths = ["tests"]
|
|
78
|
+
|
|
79
|
+
[tool.typos.default]
|
|
80
|
+
extend-ignore-re = ["[0-9a-zA-Z]{40,}"]
|
|
81
|
+
extend-words = { noteable = "noteable" }
|
|
82
|
+
|
|
83
|
+
[tool.typos.files]
|
|
84
|
+
extend-exclude = ["uv.lock", "*.log"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""GitLab API client library."""
|
|
2
|
+
|
|
3
|
+
from qodev_gitlab_api.client import GitLabClient
|
|
4
|
+
from qodev_gitlab_api.exceptions import APIError, AuthenticationError, ConfigurationError, GitLabError, NotFoundError
|
|
5
|
+
from qodev_gitlab_api.models import DiffPosition, FileFromBase64, FileFromPath, FileSource
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"APIError",
|
|
9
|
+
"AuthenticationError",
|
|
10
|
+
"ConfigurationError",
|
|
11
|
+
"DiffPosition",
|
|
12
|
+
"FileFromBase64",
|
|
13
|
+
"FileFromPath",
|
|
14
|
+
"FileSource",
|
|
15
|
+
"GitLabClient",
|
|
16
|
+
"GitLabError",
|
|
17
|
+
"NotFoundError",
|
|
18
|
+
]
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Base GitLab client mixin with HTTP primitives."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from typing import Any
|
|
6
|
+
from urllib.parse import quote
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
from dotenv import load_dotenv
|
|
10
|
+
|
|
11
|
+
from qodev_gitlab_api.exceptions import APIError, AuthenticationError, ConfigurationError, NotFoundError
|
|
12
|
+
|
|
13
|
+
load_dotenv()
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _raise_for_status(e: httpx.HTTPStatusError) -> None:
|
|
19
|
+
"""Convert httpx HTTP errors into typed exceptions."""
|
|
20
|
+
status = e.response.status_code
|
|
21
|
+
body = e.response.text[:500] if e.response.text else ""
|
|
22
|
+
if status == 401:
|
|
23
|
+
raise AuthenticationError(f"Authentication failed: {body}") from e
|
|
24
|
+
if status == 404:
|
|
25
|
+
raise NotFoundError(f"Not found: {body}", status_code=status) from e
|
|
26
|
+
raise APIError(f"API error {status}: {body}", status_code=status, response_body=body) from e
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class BaseClientMixin:
|
|
30
|
+
"""Base mixin providing HTTP primitives and initialization."""
|
|
31
|
+
|
|
32
|
+
token: str | None
|
|
33
|
+
base_url: str
|
|
34
|
+
api_url: str
|
|
35
|
+
client: httpx.Client
|
|
36
|
+
|
|
37
|
+
def __init__(self, token: str | None = None, base_url: str | None = None, validate: bool = True):
|
|
38
|
+
self.token = token or os.getenv("GITLAB_TOKEN")
|
|
39
|
+
self.base_url = (
|
|
40
|
+
base_url or os.getenv("GITLAB_BASE_URL") or os.getenv("GITLAB_URL") or "https://gitlab.com"
|
|
41
|
+
).rstrip("/")
|
|
42
|
+
|
|
43
|
+
self._validate_configuration()
|
|
44
|
+
|
|
45
|
+
self.api_url = f"{self.base_url}/api/v4"
|
|
46
|
+
headers: dict[str, str] = {"PRIVATE-TOKEN": str(self.token), "Content-Type": "application/json"}
|
|
47
|
+
self.client = httpx.Client(
|
|
48
|
+
base_url=self.api_url,
|
|
49
|
+
headers=headers,
|
|
50
|
+
timeout=30.0,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if validate:
|
|
54
|
+
self._test_connectivity()
|
|
55
|
+
else:
|
|
56
|
+
logger.info(f"GitLab client initialized for {self.base_url} (validation skipped)")
|
|
57
|
+
|
|
58
|
+
def _validate_configuration(self) -> None:
|
|
59
|
+
if not self.token:
|
|
60
|
+
raise ConfigurationError(
|
|
61
|
+
"GITLAB_TOKEN environment variable is required. Set it in your .env file or environment."
|
|
62
|
+
)
|
|
63
|
+
if not self.base_url.startswith(("http://", "https://")):
|
|
64
|
+
raise ConfigurationError(f"GITLAB_URL must start with http:// or https://, got: {self.base_url}")
|
|
65
|
+
|
|
66
|
+
def _test_connectivity(self) -> None:
|
|
67
|
+
try:
|
|
68
|
+
version_info = self.get("/version")
|
|
69
|
+
logger.info(f"Connected to GitLab {version_info.get('version', 'unknown')} at {self.base_url}")
|
|
70
|
+
except (AuthenticationError, APIError, NotFoundError):
|
|
71
|
+
raise
|
|
72
|
+
except httpx.HTTPStatusError as e:
|
|
73
|
+
_raise_for_status(e)
|
|
74
|
+
except httpx.RequestError as e:
|
|
75
|
+
raise ConfigurationError(f"Cannot connect to GitLab at {self.base_url}. Check your GITLAB_URL.") from e
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def _encode_project_id(project_id: str) -> str:
|
|
79
|
+
return quote(project_id, safe="")
|
|
80
|
+
|
|
81
|
+
def get(self, endpoint: str, params: dict[str, Any] | None = None) -> Any:
|
|
82
|
+
"""GET request to GitLab API."""
|
|
83
|
+
try:
|
|
84
|
+
logger.debug(f"GET {endpoint} with params={params}")
|
|
85
|
+
response = self.client.get(endpoint, params=params)
|
|
86
|
+
response.raise_for_status()
|
|
87
|
+
return response.json()
|
|
88
|
+
except httpx.HTTPStatusError as e:
|
|
89
|
+
logger.error(f"GitLab API error for GET {endpoint}: {e.response.status_code}")
|
|
90
|
+
_raise_for_status(e)
|
|
91
|
+
except httpx.RequestError as e:
|
|
92
|
+
logger.error(f"Network error for GET {endpoint}: {e}")
|
|
93
|
+
raise
|
|
94
|
+
|
|
95
|
+
def get_paginated(
|
|
96
|
+
self, endpoint: str, params: dict[str, Any] | None = None, per_page: int = 100, max_pages: int = 100
|
|
97
|
+
) -> list[Any]:
|
|
98
|
+
"""GET request with pagination support."""
|
|
99
|
+
params = params or {}
|
|
100
|
+
params["per_page"] = min(per_page, 100)
|
|
101
|
+
params["page"] = 1
|
|
102
|
+
|
|
103
|
+
all_results: list[Any] = []
|
|
104
|
+
pages_fetched = 0
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
while pages_fetched < max_pages:
|
|
108
|
+
logger.debug(f"GET {endpoint} page {params['page']} (per_page={params['per_page']})")
|
|
109
|
+
response = self.client.get(endpoint, params=params)
|
|
110
|
+
response.raise_for_status()
|
|
111
|
+
results = response.json()
|
|
112
|
+
|
|
113
|
+
if not results:
|
|
114
|
+
break
|
|
115
|
+
|
|
116
|
+
all_results.extend(results)
|
|
117
|
+
pages_fetched += 1
|
|
118
|
+
|
|
119
|
+
if "x-next-page" not in response.headers or not response.headers["x-next-page"]:
|
|
120
|
+
break
|
|
121
|
+
|
|
122
|
+
params["page"] += 1
|
|
123
|
+
|
|
124
|
+
if pages_fetched >= max_pages:
|
|
125
|
+
logger.warning(f"Hit max_pages limit ({max_pages}) for {endpoint}. Results may be incomplete.")
|
|
126
|
+
|
|
127
|
+
logger.debug(f"Fetched {len(all_results)} results from {pages_fetched} pages for {endpoint}")
|
|
128
|
+
return all_results
|
|
129
|
+
|
|
130
|
+
except httpx.HTTPStatusError as e:
|
|
131
|
+
logger.error(f"GitLab API error during pagination of {endpoint}: {e.response.status_code}")
|
|
132
|
+
_raise_for_status(e)
|
|
133
|
+
return [] # unreachable, for type checker
|
|
134
|
+
|
|
135
|
+
def get_projects(self, owned: bool = False, membership: bool = True) -> list[dict[str, Any]]:
|
|
136
|
+
"""Get all projects."""
|
|
137
|
+
params: dict[str, Any] = {"membership": membership, "owned": owned}
|
|
138
|
+
return self.get_paginated("/projects", params=params)
|
|
139
|
+
|
|
140
|
+
def get_project(self, project_id: str) -> dict[str, Any]:
|
|
141
|
+
"""Get a specific project by ID or path."""
|
|
142
|
+
encoded_id = self._encode_project_id(project_id)
|
|
143
|
+
return self.get(f"/projects/{encoded_id}")
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""File operations client mixin."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import binascii
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from typing import Any, cast
|
|
8
|
+
from urllib.parse import quote
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from qodev_gitlab_api._base import BaseClientMixin, _raise_for_status
|
|
13
|
+
from qodev_gitlab_api.models import FileFromPath, FileSource
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FilesMixin(BaseClientMixin):
|
|
19
|
+
"""Mixin for file operations."""
|
|
20
|
+
|
|
21
|
+
def get_file_content(self, project_id: str, file_path: str, ref: str) -> str:
|
|
22
|
+
"""Get raw file content at a specific ref."""
|
|
23
|
+
encoded_id = self._encode_project_id(project_id)
|
|
24
|
+
encoded_path = quote(file_path, safe="")
|
|
25
|
+
try:
|
|
26
|
+
response = self.client.get(
|
|
27
|
+
f"/projects/{encoded_id}/repository/files/{encoded_path}/raw",
|
|
28
|
+
params={"ref": ref},
|
|
29
|
+
)
|
|
30
|
+
response.raise_for_status()
|
|
31
|
+
return response.text
|
|
32
|
+
except httpx.HTTPStatusError as e:
|
|
33
|
+
_raise_for_status(e)
|
|
34
|
+
return ""
|
|
35
|
+
|
|
36
|
+
def upload_file(self, project_id: str, source: FileSource) -> dict[str, Any]:
|
|
37
|
+
"""Upload a file to GitLab for use in markdown."""
|
|
38
|
+
encoded_id = self._encode_project_id(project_id)
|
|
39
|
+
|
|
40
|
+
if "path" in source:
|
|
41
|
+
file_path = cast(FileFromPath, source)["path"]
|
|
42
|
+
with open(file_path, "rb") as f:
|
|
43
|
+
file_content = f.read()
|
|
44
|
+
filename = os.path.basename(file_path)
|
|
45
|
+
else:
|
|
46
|
+
try:
|
|
47
|
+
file_content = base64.b64decode(source["base64"], validate=True)
|
|
48
|
+
except binascii.Error as e:
|
|
49
|
+
raise ValueError(f"Invalid base64 data: {e}") from e
|
|
50
|
+
filename = source["filename"]
|
|
51
|
+
|
|
52
|
+
files = {"file": (filename, file_content)}
|
|
53
|
+
try:
|
|
54
|
+
response = httpx.post(
|
|
55
|
+
f"{self.api_url}/projects/{encoded_id}/uploads",
|
|
56
|
+
files=files,
|
|
57
|
+
headers={"PRIVATE-TOKEN": str(self.token)},
|
|
58
|
+
timeout=30.0,
|
|
59
|
+
)
|
|
60
|
+
response.raise_for_status()
|
|
61
|
+
return response.json()
|
|
62
|
+
except httpx.HTTPStatusError as e:
|
|
63
|
+
_raise_for_status(e)
|
|
64
|
+
return {}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Issue client mixin."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from qodev_gitlab_api._base import BaseClientMixin, _raise_for_status
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class IssuesMixin(BaseClientMixin):
|
|
14
|
+
"""Mixin for issue operations."""
|
|
15
|
+
|
|
16
|
+
def get_issues(
|
|
17
|
+
self,
|
|
18
|
+
project_id: str,
|
|
19
|
+
state: str = "opened",
|
|
20
|
+
labels: str | None = None,
|
|
21
|
+
assignee_id: int | None = None,
|
|
22
|
+
milestone: str | None = None,
|
|
23
|
+
per_page: int = 20,
|
|
24
|
+
max_pages: int = 10,
|
|
25
|
+
) -> list[dict[str, Any]]:
|
|
26
|
+
encoded_id = self._encode_project_id(project_id)
|
|
27
|
+
params: dict[str, Any] = {"state": state}
|
|
28
|
+
if labels:
|
|
29
|
+
params["labels"] = labels
|
|
30
|
+
if assignee_id is not None:
|
|
31
|
+
params["assignee_id"] = assignee_id
|
|
32
|
+
if milestone:
|
|
33
|
+
params["milestone"] = milestone
|
|
34
|
+
return self.get_paginated(
|
|
35
|
+
f"/projects/{encoded_id}/issues", params=params, per_page=per_page, max_pages=max_pages
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def get_issue(self, project_id: str, issue_iid: int) -> dict[str, Any]:
|
|
39
|
+
encoded_id = self._encode_project_id(project_id)
|
|
40
|
+
return self.get(f"/projects/{encoded_id}/issues/{issue_iid}")
|
|
41
|
+
|
|
42
|
+
def create_issue(
|
|
43
|
+
self,
|
|
44
|
+
project_id: str,
|
|
45
|
+
title: str,
|
|
46
|
+
description: str | None = None,
|
|
47
|
+
labels: str | None = None,
|
|
48
|
+
assignee_ids: list[int] | None = None,
|
|
49
|
+
milestone_id: int | None = None,
|
|
50
|
+
) -> dict[str, Any]:
|
|
51
|
+
encoded_id = self._encode_project_id(project_id)
|
|
52
|
+
data: dict[str, Any] = {"title": title}
|
|
53
|
+
if description:
|
|
54
|
+
data["description"] = description
|
|
55
|
+
if labels:
|
|
56
|
+
data["labels"] = labels
|
|
57
|
+
if assignee_ids:
|
|
58
|
+
data["assignee_ids"] = assignee_ids
|
|
59
|
+
if milestone_id is not None:
|
|
60
|
+
data["milestone_id"] = milestone_id
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
response = self.client.post(f"/projects/{encoded_id}/issues", json=data)
|
|
64
|
+
response.raise_for_status()
|
|
65
|
+
return response.json()
|
|
66
|
+
except httpx.HTTPStatusError as e:
|
|
67
|
+
_raise_for_status(e)
|
|
68
|
+
return {}
|
|
69
|
+
|
|
70
|
+
def update_issue(
|
|
71
|
+
self,
|
|
72
|
+
project_id: str,
|
|
73
|
+
issue_iid: int,
|
|
74
|
+
title: str | None = None,
|
|
75
|
+
description: str | None = None,
|
|
76
|
+
state_event: str | None = None,
|
|
77
|
+
labels: str | None = None,
|
|
78
|
+
assignee_ids: list[int] | None = None,
|
|
79
|
+
milestone_id: int | None = None,
|
|
80
|
+
) -> dict[str, Any]:
|
|
81
|
+
encoded_id = self._encode_project_id(project_id)
|
|
82
|
+
data: dict[str, Any] = {}
|
|
83
|
+
if title:
|
|
84
|
+
data["title"] = title
|
|
85
|
+
if description is not None:
|
|
86
|
+
data["description"] = description
|
|
87
|
+
if state_event:
|
|
88
|
+
data["state_event"] = state_event
|
|
89
|
+
if labels is not None:
|
|
90
|
+
data["labels"] = labels
|
|
91
|
+
if assignee_ids is not None:
|
|
92
|
+
data["assignee_ids"] = assignee_ids
|
|
93
|
+
if milestone_id is not None:
|
|
94
|
+
data["milestone_id"] = milestone_id
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
response = self.client.put(f"/projects/{encoded_id}/issues/{issue_iid}", json=data)
|
|
98
|
+
response.raise_for_status()
|
|
99
|
+
return response.json()
|
|
100
|
+
except httpx.HTTPStatusError as e:
|
|
101
|
+
_raise_for_status(e)
|
|
102
|
+
return {}
|
|
103
|
+
|
|
104
|
+
def close_issue(self, project_id: str, issue_iid: int) -> dict[str, Any]:
|
|
105
|
+
return self.update_issue(project_id, issue_iid, state_event="close")
|
|
106
|
+
|
|
107
|
+
def get_issue_notes(self, project_id: str, issue_iid: int) -> list[dict[str, Any]]:
|
|
108
|
+
encoded_id = self._encode_project_id(project_id)
|
|
109
|
+
return self.get_paginated(f"/projects/{encoded_id}/issues/{issue_iid}/notes")
|
|
110
|
+
|
|
111
|
+
def create_issue_note(self, project_id: str, issue_iid: int, body: str) -> dict[str, Any]:
|
|
112
|
+
encoded_id = self._encode_project_id(project_id)
|
|
113
|
+
try:
|
|
114
|
+
response = self.client.post(f"/projects/{encoded_id}/issues/{issue_iid}/notes", json={"body": body})
|
|
115
|
+
response.raise_for_status()
|
|
116
|
+
return response.json()
|
|
117
|
+
except httpx.HTTPStatusError as e:
|
|
118
|
+
_raise_for_status(e)
|
|
119
|
+
return {}
|