pjdev-gitlab 5.1.7__tar.gz → 5.1.10__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.
- {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/PKG-INFO +90 -4
- pjdev_gitlab-5.1.10/README.md +187 -0
- {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/pyproject.toml +3 -0
- pjdev_gitlab-5.1.10/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_work_items/SKILL.md +173 -0
- {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/__about__.py +1 -1
- {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/__init__.py +3 -0
- {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/api_utilities.py +154 -2
- {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/config_service.py +3 -0
- pjdev_gitlab-5.1.10/src/pjdev_gitlab/models.py +369 -0
- pjdev_gitlab-5.1.10/src/pjdev_gitlab/oauth_cli.py +122 -0
- pjdev_gitlab-5.1.10/src/pjdev_gitlab/oauth_service.py +354 -0
- pjdev_gitlab-5.1.10/src/pjdev_gitlab/work_items_service.py +720 -0
- pjdev_gitlab-5.1.10/test_report_pjdev-gitlab.txt +74 -0
- {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/tests/tests_for_api_utilities.py +33 -0
- pjdev_gitlab-5.1.10/tests/tests_for_oauth_cli.py +85 -0
- pjdev_gitlab-5.1.10/tests/tests_for_oauth_service.py +142 -0
- pjdev_gitlab-5.1.10/tests/tests_for_work_items_service.py +681 -0
- pjdev_gitlab-5.1.7/README.md +0 -101
- pjdev_gitlab-5.1.7/src/pjdev_gitlab/models.py +0 -173
- pjdev_gitlab-5.1.7/test_report_pjdev-gitlab.txt +0 -36
- {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/.gitignore +0 -0
- {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/LICENSE.txt +0 -0
- {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_issues/SKILL.md +0 -0
- {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_merge_requests/SKILL.md +0 -0
- {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_packages/SKILL.md +0 -0
- {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_repo_files/SKILL.md +0 -0
- {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/issues_service.py +0 -0
- {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/merge_requests_service.py +0 -0
- {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/packages_service.py +0 -0
- {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/py.typed +0 -0
- {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/repo_files_service.py +0 -0
- {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/test.sh +0 -0
- {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/tests/__init__.py +0 -0
- {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/tests/conftest.py +0 -0
- {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/tests/tests_for_issues_service.py +0 -0
- {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/tests/tests_for_merge_requests_service.py +0 -0
- {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/tests/tests_for_models.py +0 -0
- {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/tests/tests_for_packages_service.py +0 -0
- {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/tests/tests_for_repo_files_service.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pjdev-gitlab
|
|
3
|
-
Version: 5.1.
|
|
3
|
+
Version: 5.1.10
|
|
4
4
|
Project-URL: Documentation, https://gitlab.purplejay.io/keystone/python/-/tree/main/pjdev-gitlab/README.md
|
|
5
5
|
Project-URL: Issues, https://gitlab.purplejay.io/keystone/python/-/issues
|
|
6
6
|
Project-URL: Source, https://gitlab.purplejay.io/keystone/python
|
|
@@ -31,7 +31,7 @@ Description-Content-Type: text/markdown
|
|
|
31
31
|
[](https://pypi.org/project/pjdev-gitlab)
|
|
32
32
|
[](https://pypi.org/project/pjdev-gitlab)
|
|
33
33
|
|
|
34
|
-
Async GitLab automation SDK. Wraps the GitLab REST API v4 directly with `httpx.AsyncClient` and Pydantic models. Covers issues, merge requests, repository files, and the generic package registry.
|
|
34
|
+
Async GitLab automation SDK. Wraps both the GitLab REST API v4 and GraphQL API directly with `httpx.AsyncClient` and Pydantic models. Covers issues, workItems (the GraphQL replacement for issues — status, comments, labels), merge requests, repository files, and the generic package registry.
|
|
35
35
|
|
|
36
36
|
-----
|
|
37
37
|
|
|
@@ -48,11 +48,55 @@ via `init()`):
|
|
|
48
48
|
|
|
49
49
|
| variable | required | purpose |
|
|
50
50
|
| --- | --- | --- |
|
|
51
|
-
| `GL_TOKEN` | yes |
|
|
51
|
+
| `GL_TOKEN` | yes | access token (`api` scope) — a PAT, or an OAuth token with `GL_AUTH_SCHEME=bearer` |
|
|
52
52
|
| `GL_GITLAB_URL` | yes | base URL, e.g. `https://gitlab.com` |
|
|
53
|
+
| `GL_AUTH_SCHEME` | no | `private-token` (default, PAT header) or `bearer` (OAuth access token) |
|
|
53
54
|
| `GL_DEFAULT_PROJECT_ID` | no | default project for service helpers |
|
|
54
55
|
| `GL_OUTPUT_PATH` | no | directory for downloaded files |
|
|
55
56
|
|
|
57
|
+
### OAuth 2.0 (per-user attribution)
|
|
58
|
+
|
|
59
|
+
To attribute API writes to a real person instead of a shared bot token, mint a
|
|
60
|
+
short-lived OAuth access token with the interactive Authorization Code + PKCE
|
|
61
|
+
flow and initialize with `auth_scheme="bearer"`:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from pjdev_gitlab import config_service, oauth_service
|
|
65
|
+
|
|
66
|
+
# Opens a browser for sign-in/consent on first run; caches + refreshes the token
|
|
67
|
+
# per host under ~/.config/pjdev-gitlab/tokens/ (0600) on subsequent runs.
|
|
68
|
+
token = oauth_service.get_access_token(
|
|
69
|
+
gitlab_url="https://gitlab.example.com",
|
|
70
|
+
client_id="<registered PKCE app client id>",
|
|
71
|
+
)
|
|
72
|
+
config_service.init(
|
|
73
|
+
token=token, gitlab_url="https://gitlab.example.com", auth_scheme="bearer"
|
|
74
|
+
)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The OAuth application must be registered as a non-confidential (PKCE) app with
|
|
78
|
+
**scope** `api` and **Redirect URI** exactly `http://localhost:7331/callback`
|
|
79
|
+
(override the port via `get_access_token(redirect_port=...)`). A `bearer` token
|
|
80
|
+
is sent as `Authorization: Bearer <token>`; the default `private-token` scheme
|
|
81
|
+
sends `PRIVATE-TOKEN` for personal/project/group access tokens.
|
|
82
|
+
|
|
83
|
+
#### `pjdev-gitlab-auth` console script
|
|
84
|
+
|
|
85
|
+
The same flow is exposed as a console script, so shell callers (e.g. skills) can
|
|
86
|
+
mint a token without embedding any Python. It prints only the token to stdout and
|
|
87
|
+
status to stderr:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
TOKEN="$(uvx --from 'pjdev-gitlab' pjdev-gitlab-auth \
|
|
91
|
+
--gitlab-url https://gitlab.example.com --client-id <client-id>)"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The library is instance-agnostic: `--client-id` (or `GL_OAUTH_CLIENT_ID`) is
|
|
95
|
+
required and the host defaults to `https://gitlab.com` (override with
|
|
96
|
+
`--gitlab-url` / `GL_GITLAB_URL`). Other flags: `--client-secret`,
|
|
97
|
+
`--redirect-port`, `--scopes`, `--force` (each with a `GL_OAUTH_*` env
|
|
98
|
+
equivalent).
|
|
99
|
+
|
|
56
100
|
### Recommended: 1Password + `op run`
|
|
57
101
|
|
|
58
102
|
On a developer laptop, keep the token in 1Password and inject it into the host
|
|
@@ -120,9 +164,51 @@ asyncio.run(main())
|
|
|
120
164
|
|
|
121
165
|
Run it: `op run --env-file=.env.op -- python my_script.py`.
|
|
122
166
|
|
|
167
|
+
### WorkItems (GraphQL)
|
|
168
|
+
|
|
169
|
+
`workItem` is GitLab's unified replacement for the legacy `Issue` type — use
|
|
170
|
+
`work_items_service` for status/comments/labels on modern GitLab instances:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
import asyncio
|
|
174
|
+
from pjdev_gitlab import config_service, work_items_service
|
|
175
|
+
from pjdev_gitlab.models import WorkItemState, WorkItemStateEvent
|
|
176
|
+
|
|
177
|
+
async def main() -> None:
|
|
178
|
+
config_service.init()
|
|
179
|
+
|
|
180
|
+
open_bugs = await work_items_service.search_work_items(
|
|
181
|
+
project_path="my-group/my-project",
|
|
182
|
+
state=WorkItemState.OPEN,
|
|
183
|
+
labels=["bug"],
|
|
184
|
+
search="timeout",
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
label = await work_items_service.create_label(
|
|
188
|
+
"needs-review", project_path="my-group/my-project", color="#FFAA00"
|
|
189
|
+
)
|
|
190
|
+
await work_items_service.set_work_item_labels(
|
|
191
|
+
open_bugs[0].iid,
|
|
192
|
+
[label.id],
|
|
193
|
+
mode="add",
|
|
194
|
+
project_path="my-group/my-project",
|
|
195
|
+
)
|
|
196
|
+
await work_items_service.comment_on_work_item(
|
|
197
|
+
open_bugs[0].iid,
|
|
198
|
+
"Triaged — assigning a reviewer.",
|
|
199
|
+
project_path="my-group/my-project",
|
|
200
|
+
)
|
|
201
|
+
await work_items_service.set_work_item_state(
|
|
202
|
+
open_bugs[0].iid, WorkItemStateEvent.CLOSE,
|
|
203
|
+
project_path="my-group/my-project",
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
asyncio.run(main())
|
|
207
|
+
```
|
|
208
|
+
|
|
123
209
|
## Bundled agent skills
|
|
124
210
|
|
|
125
|
-
Skill files for AI agents ship under `.agents/skills/` inside the installed package, following the [library-skills.io](https://library-skills.io/create/) convention. Topics: issues, merge requests, repository files, generic packages.
|
|
211
|
+
Skill files for AI agents ship under `.agents/skills/` inside the installed package, following the [library-skills.io](https://library-skills.io/create/) convention. Topics: issues, workItems, merge requests, repository files, generic packages.
|
|
126
212
|
|
|
127
213
|
## License
|
|
128
214
|
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# pjdev-gitlab
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/pjdev-gitlab)
|
|
4
|
+
[](https://pypi.org/project/pjdev-gitlab)
|
|
5
|
+
|
|
6
|
+
Async GitLab automation SDK. Wraps both the GitLab REST API v4 and GraphQL API directly with `httpx.AsyncClient` and Pydantic models. Covers issues, workItems (the GraphQL replacement for issues — status, comments, labels), merge requests, repository files, and the generic package registry.
|
|
7
|
+
|
|
8
|
+
-----
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```console
|
|
13
|
+
pip install pjdev-gitlab
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Configuration
|
|
17
|
+
|
|
18
|
+
`pjdev-gitlab` reads `GL_*` environment variables (or accepts the same values
|
|
19
|
+
via `init()`):
|
|
20
|
+
|
|
21
|
+
| variable | required | purpose |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| `GL_TOKEN` | yes | access token (`api` scope) — a PAT, or an OAuth token with `GL_AUTH_SCHEME=bearer` |
|
|
24
|
+
| `GL_GITLAB_URL` | yes | base URL, e.g. `https://gitlab.com` |
|
|
25
|
+
| `GL_AUTH_SCHEME` | no | `private-token` (default, PAT header) or `bearer` (OAuth access token) |
|
|
26
|
+
| `GL_DEFAULT_PROJECT_ID` | no | default project for service helpers |
|
|
27
|
+
| `GL_OUTPUT_PATH` | no | directory for downloaded files |
|
|
28
|
+
|
|
29
|
+
### OAuth 2.0 (per-user attribution)
|
|
30
|
+
|
|
31
|
+
To attribute API writes to a real person instead of a shared bot token, mint a
|
|
32
|
+
short-lived OAuth access token with the interactive Authorization Code + PKCE
|
|
33
|
+
flow and initialize with `auth_scheme="bearer"`:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from pjdev_gitlab import config_service, oauth_service
|
|
37
|
+
|
|
38
|
+
# Opens a browser for sign-in/consent on first run; caches + refreshes the token
|
|
39
|
+
# per host under ~/.config/pjdev-gitlab/tokens/ (0600) on subsequent runs.
|
|
40
|
+
token = oauth_service.get_access_token(
|
|
41
|
+
gitlab_url="https://gitlab.example.com",
|
|
42
|
+
client_id="<registered PKCE app client id>",
|
|
43
|
+
)
|
|
44
|
+
config_service.init(
|
|
45
|
+
token=token, gitlab_url="https://gitlab.example.com", auth_scheme="bearer"
|
|
46
|
+
)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The OAuth application must be registered as a non-confidential (PKCE) app with
|
|
50
|
+
**scope** `api` and **Redirect URI** exactly `http://localhost:7331/callback`
|
|
51
|
+
(override the port via `get_access_token(redirect_port=...)`). A `bearer` token
|
|
52
|
+
is sent as `Authorization: Bearer <token>`; the default `private-token` scheme
|
|
53
|
+
sends `PRIVATE-TOKEN` for personal/project/group access tokens.
|
|
54
|
+
|
|
55
|
+
#### `pjdev-gitlab-auth` console script
|
|
56
|
+
|
|
57
|
+
The same flow is exposed as a console script, so shell callers (e.g. skills) can
|
|
58
|
+
mint a token without embedding any Python. It prints only the token to stdout and
|
|
59
|
+
status to stderr:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
TOKEN="$(uvx --from 'pjdev-gitlab' pjdev-gitlab-auth \
|
|
63
|
+
--gitlab-url https://gitlab.example.com --client-id <client-id>)"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The library is instance-agnostic: `--client-id` (or `GL_OAUTH_CLIENT_ID`) is
|
|
67
|
+
required and the host defaults to `https://gitlab.com` (override with
|
|
68
|
+
`--gitlab-url` / `GL_GITLAB_URL`). Other flags: `--client-secret`,
|
|
69
|
+
`--redirect-port`, `--scopes`, `--force` (each with a `GL_OAUTH_*` env
|
|
70
|
+
equivalent).
|
|
71
|
+
|
|
72
|
+
### Recommended: 1Password + `op run`
|
|
73
|
+
|
|
74
|
+
On a developer laptop, keep the token in 1Password and inject it into the host
|
|
75
|
+
process with [`op run`](https://developer.1password.com/docs/cli/secrets-environment-variables/) —
|
|
76
|
+
the secret never sits in your shell environment or on disk in plaintext.
|
|
77
|
+
|
|
78
|
+
1. Install the 1Password CLI (`brew install --cask 1password-cli`) and turn on
|
|
79
|
+
**Settings → Developer → Integrate with 1Password CLI** in the desktop app.
|
|
80
|
+
2. Store the token in 1Password (e.g. an API Credential titled
|
|
81
|
+
`GitLab — purplejay` with a `credential` field).
|
|
82
|
+
3. Drop a committable `.env.op` next to your project — references only, no
|
|
83
|
+
real secrets:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# .env.op
|
|
87
|
+
GL_TOKEN="op://Private/GitLab — purplejay/credential"
|
|
88
|
+
GL_GITLAB_URL="https://gitlab.purplejay.io"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
4. Launch your script — or the entire Claude Code session that will use this
|
|
92
|
+
library — under `op run`:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
op run --env-file=.env.op -- python my_script.py
|
|
96
|
+
op run --env-file=.env.op -- claude
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`op run` resolves the references, exports them to the subprocess, and tears
|
|
100
|
+
them down on exit. Inside Python, just call `config_service.init()` with no
|
|
101
|
+
arguments and the values flow in from the environment.
|
|
102
|
+
|
|
103
|
+
In CI, skip 1Password and set `GL_TOKEN`/`GL_GITLAB_URL` from the job's
|
|
104
|
+
existing variables (e.g. `CI_JOB_TOKEN` for project-scoped operations).
|
|
105
|
+
|
|
106
|
+
## Usage
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
import asyncio
|
|
110
|
+
from pjdev_gitlab import config_service, issues_service
|
|
111
|
+
from pjdev_gitlab.models import StateEvent
|
|
112
|
+
|
|
113
|
+
async def main() -> None:
|
|
114
|
+
# Token & URL come from GL_TOKEN / GL_GITLAB_URL (e.g. via `op run`).
|
|
115
|
+
config_service.init(default_project_id="my-group/my-project")
|
|
116
|
+
|
|
117
|
+
issue = await issues_service.create_issue(
|
|
118
|
+
project_id="my-group/my-project",
|
|
119
|
+
title="Bug: timeout on /widgets",
|
|
120
|
+
description="The endpoint times out under load.\n\n/label ~bug ~priority::high",
|
|
121
|
+
labels=["bug"],
|
|
122
|
+
)
|
|
123
|
+
await issues_service.comment_on_issue(
|
|
124
|
+
project_id="my-group/my-project",
|
|
125
|
+
issue_iid=issue.iid,
|
|
126
|
+
body="Investigating now.",
|
|
127
|
+
)
|
|
128
|
+
await issues_service.set_issue_state(
|
|
129
|
+
project_id="my-group/my-project",
|
|
130
|
+
issue_iid=issue.iid,
|
|
131
|
+
state_event=StateEvent.close,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
asyncio.run(main())
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Run it: `op run --env-file=.env.op -- python my_script.py`.
|
|
138
|
+
|
|
139
|
+
### WorkItems (GraphQL)
|
|
140
|
+
|
|
141
|
+
`workItem` is GitLab's unified replacement for the legacy `Issue` type — use
|
|
142
|
+
`work_items_service` for status/comments/labels on modern GitLab instances:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
import asyncio
|
|
146
|
+
from pjdev_gitlab import config_service, work_items_service
|
|
147
|
+
from pjdev_gitlab.models import WorkItemState, WorkItemStateEvent
|
|
148
|
+
|
|
149
|
+
async def main() -> None:
|
|
150
|
+
config_service.init()
|
|
151
|
+
|
|
152
|
+
open_bugs = await work_items_service.search_work_items(
|
|
153
|
+
project_path="my-group/my-project",
|
|
154
|
+
state=WorkItemState.OPEN,
|
|
155
|
+
labels=["bug"],
|
|
156
|
+
search="timeout",
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
label = await work_items_service.create_label(
|
|
160
|
+
"needs-review", project_path="my-group/my-project", color="#FFAA00"
|
|
161
|
+
)
|
|
162
|
+
await work_items_service.set_work_item_labels(
|
|
163
|
+
open_bugs[0].iid,
|
|
164
|
+
[label.id],
|
|
165
|
+
mode="add",
|
|
166
|
+
project_path="my-group/my-project",
|
|
167
|
+
)
|
|
168
|
+
await work_items_service.comment_on_work_item(
|
|
169
|
+
open_bugs[0].iid,
|
|
170
|
+
"Triaged — assigning a reviewer.",
|
|
171
|
+
project_path="my-group/my-project",
|
|
172
|
+
)
|
|
173
|
+
await work_items_service.set_work_item_state(
|
|
174
|
+
open_bugs[0].iid, WorkItemStateEvent.CLOSE,
|
|
175
|
+
project_path="my-group/my-project",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
asyncio.run(main())
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Bundled agent skills
|
|
182
|
+
|
|
183
|
+
Skill files for AI agents ship under `.agents/skills/` inside the installed package, following the [library-skills.io](https://library-skills.io/create/) convention. Topics: issues, workItems, merge requests, repository files, generic packages.
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
`pjdev-gitlab` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pjdev_gitlab_work_items
|
|
3
|
+
description: Use this skill to automate GitLab workItems via pjdev-gitlab — the GraphQL-backed successor to issues. Covers searching/filtering by project or group with pagination, viewing/updating status, reading/posting/replying to comments, and managing labels (including creating new project or group labels). Trigger when the user asks to query, create, or modify GitLab workItems (or "issues" on a recent GitLab instance) programmatically.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# pjdev_gitlab_work_items
|
|
7
|
+
|
|
8
|
+
Async helpers for GitLab workItems, built on `httpx.AsyncClient` against the
|
|
9
|
+
[GitLab GraphQL API](https://docs.gitlab.com/api/graphql/).
|
|
10
|
+
|
|
11
|
+
`workItem` is GitLab's unified replacement for the legacy `Issue` type. New
|
|
12
|
+
behavior (custom statuses, hierarchy, the typed-issuable system) lives on
|
|
13
|
+
workItems and is only reachable via GraphQL. Use this module instead of
|
|
14
|
+
`issues_service` when working on a modern GitLab instance.
|
|
15
|
+
|
|
16
|
+
Reference docs (source of truth for behavior):
|
|
17
|
+
- [GraphQL API](https://docs.gitlab.com/api/graphql/)
|
|
18
|
+
- [GraphQL reference](https://docs.gitlab.com/api/graphql/reference/)
|
|
19
|
+
- [WorkItems](https://docs.gitlab.com/user/work_items/)
|
|
20
|
+
- [Labels](https://docs.gitlab.com/user/project/labels/)
|
|
21
|
+
- [Comments and threads](https://docs.gitlab.com/user/discussions/)
|
|
22
|
+
|
|
23
|
+
## Setup
|
|
24
|
+
|
|
25
|
+
`pjdev_gitlab` reads `GL_TOKEN` and `GL_GITLAB_URL` from the environment.
|
|
26
|
+
Recommended pattern: keep the token in 1Password and inject it with
|
|
27
|
+
[`op run`](https://developer.1password.com/docs/cli/secrets-environment-variables/).
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# .env.op
|
|
31
|
+
GL_TOKEN="op://Private/GitLab — <host>/credential"
|
|
32
|
+
GL_GITLAB_URL="https://gitlab.example.com"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
op run --env-file=.env.op -- python my_script.py
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from pjdev_gitlab import config_service, work_items_service
|
|
41
|
+
|
|
42
|
+
config_service.init() # token/url from env
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The token needs `api` scope (mutations) or at least `read_api` (queries only).
|
|
46
|
+
|
|
47
|
+
## Identifiers
|
|
48
|
+
|
|
49
|
+
Every workItem-facing helper accepts either:
|
|
50
|
+
|
|
51
|
+
- A **Global ID** like `gid://gitlab/WorkItem/12345`.
|
|
52
|
+
- A **numeric DB id** (`12345`), which is auto-coerced to a Global ID.
|
|
53
|
+
- A project/group-scoped **iid** (the number users see in the URL) plus
|
|
54
|
+
`project_path=...` or `group_path=...` — the helper resolves it to a
|
|
55
|
+
Global ID with one extra query.
|
|
56
|
+
|
|
57
|
+
Pick whichever you have on hand:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
# By iid (common when scripting against URLs the user shared)
|
|
61
|
+
item = await work_items_service.get_work_item(42, project_path="group/proj")
|
|
62
|
+
|
|
63
|
+
# By Global ID (zero-lookup path)
|
|
64
|
+
item = await work_items_service.get_work_item("gid://gitlab/WorkItem/12345")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Search & filter
|
|
68
|
+
|
|
69
|
+
`search_work_items` walks the namespace's `workItems` connection and follows
|
|
70
|
+
`pageInfo.endCursor` until exhausted (set `paginate_results=False` for a
|
|
71
|
+
single page). Either `project_path` or `group_path` is required.
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from pjdev_gitlab.models import WorkItemState
|
|
75
|
+
|
|
76
|
+
open_bugs = await work_items_service.search_work_items(
|
|
77
|
+
project_path="group/proj",
|
|
78
|
+
state=WorkItemState.OPEN,
|
|
79
|
+
labels=["bug"],
|
|
80
|
+
search="timeout", # matches title/description
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
across_group = await work_items_service.search_work_items(
|
|
84
|
+
group_path="group",
|
|
85
|
+
state=WorkItemState.OPEN,
|
|
86
|
+
page_size=50,
|
|
87
|
+
)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## View / update status
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from pjdev_gitlab.models import WorkItemStateEvent
|
|
94
|
+
|
|
95
|
+
item = await work_items_service.get_work_item(42, project_path="group/proj")
|
|
96
|
+
print(item.state) # WorkItemState.OPEN | CLOSED
|
|
97
|
+
|
|
98
|
+
await work_items_service.set_work_item_state(
|
|
99
|
+
42, WorkItemStateEvent.CLOSE, project_path="group/proj"
|
|
100
|
+
)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Comments and threaded replies
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
# Read all human comments (system notes filtered out by default)
|
|
107
|
+
notes = await work_items_service.list_work_item_notes(
|
|
108
|
+
42, project_path="group/proj"
|
|
109
|
+
)
|
|
110
|
+
for n in notes:
|
|
111
|
+
print(n.author.username if n.author else "?", "->", n.body)
|
|
112
|
+
|
|
113
|
+
# Post a new top-level comment (starts a new discussion thread)
|
|
114
|
+
await work_items_service.comment_on_work_item(
|
|
115
|
+
42, "Looking into this now.", project_path="group/proj"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Reply to an existing thread
|
|
119
|
+
discussions = await work_items_service.list_work_item_discussions(
|
|
120
|
+
42, project_path="group/proj"
|
|
121
|
+
)
|
|
122
|
+
await work_items_service.reply_to_work_item_discussion(
|
|
123
|
+
42,
|
|
124
|
+
discussion_id=discussions[0].id,
|
|
125
|
+
body="Confirmed — pushing a fix shortly.",
|
|
126
|
+
project_path="group/proj",
|
|
127
|
+
)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Manage labels
|
|
131
|
+
|
|
132
|
+
Label management uses a **delta API** — add or remove specific labels. There
|
|
133
|
+
is no "replace" mode; compute the diff yourself if you need one.
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
# Look up labels (includes parent group labels for projects)
|
|
137
|
+
labels = await work_items_service.list_labels(
|
|
138
|
+
project_path="group/proj", search="prio"
|
|
139
|
+
)
|
|
140
|
+
# [ProjectLabel(id='gid://gitlab/ProjectLabel/7', title='priority::high', ...), ...]
|
|
141
|
+
|
|
142
|
+
# Apply / remove on a workItem
|
|
143
|
+
await work_items_service.set_work_item_labels(
|
|
144
|
+
42, [labels[0].id], mode="add", project_path="group/proj"
|
|
145
|
+
)
|
|
146
|
+
await work_items_service.set_work_item_labels(
|
|
147
|
+
42, [labels[0].id], mode="remove", project_path="group/proj"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Create a brand-new project label
|
|
151
|
+
new_label = await work_items_service.create_label(
|
|
152
|
+
"needs-review",
|
|
153
|
+
project_path="group/proj",
|
|
154
|
+
color="#FFAA00",
|
|
155
|
+
description="Awaiting reviewer triage",
|
|
156
|
+
)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Group labels (visible to all child projects) are created the same way with
|
|
160
|
+
`group_path=` instead of `project_path=`.
|
|
161
|
+
|
|
162
|
+
## Tips for AI agents
|
|
163
|
+
|
|
164
|
+
- Prefer batching label changes into one `set_work_item_labels(mode="add", labels=[a, b, c])`
|
|
165
|
+
call — that's one mutation instead of three.
|
|
166
|
+
- When you only have an iid, pass `project_path=` once and reuse the
|
|
167
|
+
`WorkItem.id` (Global ID) on follow-up mutations to skip the lookup round-trip.
|
|
168
|
+
- `WorkItem.state` is `OPEN`/`CLOSED` (uppercase, matches the GraphQL enum
|
|
169
|
+
directly), but the **filter** uses lowercase `opened`/`closed` — the helper
|
|
170
|
+
translates `WorkItemState.OPEN` automatically when you pass it to
|
|
171
|
+
`search_work_items(state=...)`.
|
|
172
|
+
- A workItem returned by a mutation always reflects the post-mutation state,
|
|
173
|
+
so you can read `result.state` / `result.labels` instead of re-fetching.
|