pjdev-gitlab 5.1.12__tar.gz → 5.1.14__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.12 → pjdev_gitlab-5.1.14}/PKG-INFO +89 -2
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/README.md +87 -0
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_work_items/SKILL.md +34 -1
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/src/pjdev_gitlab/__about__.py +1 -1
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/src/pjdev_gitlab/__init__.py +1 -0
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/src/pjdev_gitlab/issues_service.py +26 -0
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/src/pjdev_gitlab/merge_requests_service.py +16 -10
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/src/pjdev_gitlab/models.py +34 -0
- pjdev_gitlab-5.1.14/src/pjdev_gitlab/notes_service.py +294 -0
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/src/pjdev_gitlab/work_items_service.py +118 -6
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/test_report_pjdev-gitlab.txt +108 -85
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/tests/tests_for_issues_service.py +21 -0
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/tests/tests_for_merge_requests_service.py +23 -0
- pjdev_gitlab-5.1.14/tests/tests_for_notes_service.py +250 -0
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/tests/tests_for_work_items_service.py +188 -0
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/.gitignore +0 -0
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/LICENSE.txt +0 -0
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/pyproject.toml +0 -0
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_issues/SKILL.md +0 -0
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_merge_requests/SKILL.md +0 -0
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_packages/SKILL.md +0 -0
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_repo_files/SKILL.md +0 -0
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/src/pjdev_gitlab/api_utilities.py +0 -0
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/src/pjdev_gitlab/config_service.py +0 -0
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/src/pjdev_gitlab/git_sync_service.py +0 -0
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/src/pjdev_gitlab/oauth_cli.py +0 -0
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/src/pjdev_gitlab/oauth_service.py +0 -0
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/src/pjdev_gitlab/packages_service.py +0 -0
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/src/pjdev_gitlab/py.typed +0 -0
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/src/pjdev_gitlab/repo_files_service.py +0 -0
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/src/pjdev_gitlab/sync_cli.py +0 -0
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/test.sh +0 -0
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/tests/__init__.py +0 -0
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/tests/conftest.py +0 -0
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/tests/tests_for_api_utilities.py +0 -0
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/tests/tests_for_git_sync_service.py +0 -0
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/tests/tests_for_models.py +0 -0
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/tests/tests_for_oauth_cli.py +0 -0
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/tests/tests_for_oauth_service.py +0 -0
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/tests/tests_for_packages_service.py +0 -0
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/tests/tests_for_repo_files_service.py +0 -0
- {pjdev_gitlab-5.1.12 → pjdev_gitlab-5.1.14}/tests/tests_for_sync_cli.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: pjdev-gitlab
|
|
3
|
-
Version: 5.1.
|
|
3
|
+
Version: 5.1.14
|
|
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
|
|
@@ -192,6 +192,68 @@ asyncio.run(main())
|
|
|
192
192
|
|
|
193
193
|
Run it: `op run --env-file=.env.op -- python my_script.py`.
|
|
194
194
|
|
|
195
|
+
### Notes (comments) and replies
|
|
196
|
+
|
|
197
|
+
`notes_service` reads and replies to comments on **both** issues and merge
|
|
198
|
+
requests — they share the same endpoint shape, so one `Noteable` parameter
|
|
199
|
+
selects which.
|
|
200
|
+
|
|
201
|
+
GitLab only lets you reply to a *discussion* (a thread), never to a note id, so
|
|
202
|
+
`reply_to_note` resolves the note to its containing thread for you:
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
import asyncio
|
|
206
|
+
from pjdev_gitlab import config_service, notes_service
|
|
207
|
+
from pjdev_gitlab.notes_service import Noteable, QuickActionOnlyError
|
|
208
|
+
|
|
209
|
+
async def main() -> None:
|
|
210
|
+
config_service.init()
|
|
211
|
+
|
|
212
|
+
# Fetch. Notes come back newest-first; pass sort="asc" to read in order.
|
|
213
|
+
notes = await notes_service.list_notes(
|
|
214
|
+
"my-group/my-project", Noteable.merge_request, 166,
|
|
215
|
+
include_system=False, # drop "assigned to @x", "changed milestone", ...
|
|
216
|
+
sort="asc",
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# Threads, when you need a discussion id to answer into.
|
|
220
|
+
threads = await notes_service.list_discussions(
|
|
221
|
+
"my-group/my-project", Noteable.merge_request, 166
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Reply, addressing the note you're answering.
|
|
225
|
+
await notes_service.reply_to_note(
|
|
226
|
+
"my-group/my-project", Noteable.merge_request, 166,
|
|
227
|
+
note_id=notes[0].id,
|
|
228
|
+
body="Good catch — fixed in the next push.",
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Or straight into a known thread.
|
|
232
|
+
await notes_service.reply_to_discussion(
|
|
233
|
+
"my-group/my-project", Noteable.issue, 730,
|
|
234
|
+
discussion_id=threads[0].id,
|
|
235
|
+
body="Verified on TEST1.",
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
asyncio.run(main())
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
`list_merge_request_notes` / `list_issue_notes` remain as thin aliases for
|
|
242
|
+
`list_notes`.
|
|
243
|
+
|
|
244
|
+
A body made up of **only** quick actions (e.g. `/milestone %"v2.0.6"`) creates no
|
|
245
|
+
note — GitLab runs the command and returns the commands it executed instead. The
|
|
246
|
+
reply helpers raise `QuickActionOnlyError` for that case, carrying the outcome so
|
|
247
|
+
you can confirm the side effect landed:
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
try:
|
|
251
|
+
await notes_service.reply_to_discussion(..., body='/milestone %"v2.0.6"')
|
|
252
|
+
except QuickActionOnlyError as exc:
|
|
253
|
+
print(exc.summary) # ['Set milestone to %"v2.0.6".']
|
|
254
|
+
print(exc.commands_changes) # {'milestone': {...}}
|
|
255
|
+
```
|
|
256
|
+
|
|
195
257
|
### WorkItems (GraphQL)
|
|
196
258
|
|
|
197
259
|
`workItem` is GitLab's unified replacement for the legacy `Issue` type — use
|
|
@@ -234,6 +296,31 @@ async def main() -> None:
|
|
|
234
296
|
asyncio.run(main())
|
|
235
297
|
```
|
|
236
298
|
|
|
299
|
+
#### Custom statuses
|
|
300
|
+
|
|
301
|
+
The workflow status an instance defines for itself (`Ready`, `In progress`, ...)
|
|
302
|
+
is neither a label nor the open/closed `state`, and REST does not expose it.
|
|
303
|
+
Reading it is opt-in, because GitLab ships the status widget as an experiment
|
|
304
|
+
(**17.11+**) and selecting it against an older instance fails the whole query:
|
|
305
|
+
|
|
306
|
+
```python
|
|
307
|
+
# Just the status — one small query. Preferred on a hot path.
|
|
308
|
+
status = await work_items_service.get_work_item_status(
|
|
309
|
+
42, project_path="my-group/my-project"
|
|
310
|
+
)
|
|
311
|
+
print(status.name if status else "no status set")
|
|
312
|
+
|
|
313
|
+
# Or folded into a read you were making anyway.
|
|
314
|
+
item = await work_items_service.get_work_item(
|
|
315
|
+
42, project_path="my-group/my-project", include_status=True
|
|
316
|
+
)
|
|
317
|
+
items = await work_items_service.search_work_items(
|
|
318
|
+
project_path="my-group/my-project", include_status=True
|
|
319
|
+
)
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Status names are configured per namespace — match them case-insensitively.
|
|
323
|
+
|
|
237
324
|
## Bundled agent skills
|
|
238
325
|
|
|
239
326
|
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.
|
|
@@ -164,6 +164,68 @@ asyncio.run(main())
|
|
|
164
164
|
|
|
165
165
|
Run it: `op run --env-file=.env.op -- python my_script.py`.
|
|
166
166
|
|
|
167
|
+
### Notes (comments) and replies
|
|
168
|
+
|
|
169
|
+
`notes_service` reads and replies to comments on **both** issues and merge
|
|
170
|
+
requests — they share the same endpoint shape, so one `Noteable` parameter
|
|
171
|
+
selects which.
|
|
172
|
+
|
|
173
|
+
GitLab only lets you reply to a *discussion* (a thread), never to a note id, so
|
|
174
|
+
`reply_to_note` resolves the note to its containing thread for you:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
import asyncio
|
|
178
|
+
from pjdev_gitlab import config_service, notes_service
|
|
179
|
+
from pjdev_gitlab.notes_service import Noteable, QuickActionOnlyError
|
|
180
|
+
|
|
181
|
+
async def main() -> None:
|
|
182
|
+
config_service.init()
|
|
183
|
+
|
|
184
|
+
# Fetch. Notes come back newest-first; pass sort="asc" to read in order.
|
|
185
|
+
notes = await notes_service.list_notes(
|
|
186
|
+
"my-group/my-project", Noteable.merge_request, 166,
|
|
187
|
+
include_system=False, # drop "assigned to @x", "changed milestone", ...
|
|
188
|
+
sort="asc",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Threads, when you need a discussion id to answer into.
|
|
192
|
+
threads = await notes_service.list_discussions(
|
|
193
|
+
"my-group/my-project", Noteable.merge_request, 166
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Reply, addressing the note you're answering.
|
|
197
|
+
await notes_service.reply_to_note(
|
|
198
|
+
"my-group/my-project", Noteable.merge_request, 166,
|
|
199
|
+
note_id=notes[0].id,
|
|
200
|
+
body="Good catch — fixed in the next push.",
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# Or straight into a known thread.
|
|
204
|
+
await notes_service.reply_to_discussion(
|
|
205
|
+
"my-group/my-project", Noteable.issue, 730,
|
|
206
|
+
discussion_id=threads[0].id,
|
|
207
|
+
body="Verified on TEST1.",
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
asyncio.run(main())
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`list_merge_request_notes` / `list_issue_notes` remain as thin aliases for
|
|
214
|
+
`list_notes`.
|
|
215
|
+
|
|
216
|
+
A body made up of **only** quick actions (e.g. `/milestone %"v2.0.6"`) creates no
|
|
217
|
+
note — GitLab runs the command and returns the commands it executed instead. The
|
|
218
|
+
reply helpers raise `QuickActionOnlyError` for that case, carrying the outcome so
|
|
219
|
+
you can confirm the side effect landed:
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
try:
|
|
223
|
+
await notes_service.reply_to_discussion(..., body='/milestone %"v2.0.6"')
|
|
224
|
+
except QuickActionOnlyError as exc:
|
|
225
|
+
print(exc.summary) # ['Set milestone to %"v2.0.6".']
|
|
226
|
+
print(exc.commands_changes) # {'milestone': {...}}
|
|
227
|
+
```
|
|
228
|
+
|
|
167
229
|
### WorkItems (GraphQL)
|
|
168
230
|
|
|
169
231
|
`workItem` is GitLab's unified replacement for the legacy `Issue` type — use
|
|
@@ -206,6 +268,31 @@ async def main() -> None:
|
|
|
206
268
|
asyncio.run(main())
|
|
207
269
|
```
|
|
208
270
|
|
|
271
|
+
#### Custom statuses
|
|
272
|
+
|
|
273
|
+
The workflow status an instance defines for itself (`Ready`, `In progress`, ...)
|
|
274
|
+
is neither a label nor the open/closed `state`, and REST does not expose it.
|
|
275
|
+
Reading it is opt-in, because GitLab ships the status widget as an experiment
|
|
276
|
+
(**17.11+**) and selecting it against an older instance fails the whole query:
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
# Just the status — one small query. Preferred on a hot path.
|
|
280
|
+
status = await work_items_service.get_work_item_status(
|
|
281
|
+
42, project_path="my-group/my-project"
|
|
282
|
+
)
|
|
283
|
+
print(status.name if status else "no status set")
|
|
284
|
+
|
|
285
|
+
# Or folded into a read you were making anyway.
|
|
286
|
+
item = await work_items_service.get_work_item(
|
|
287
|
+
42, project_path="my-group/my-project", include_status=True
|
|
288
|
+
)
|
|
289
|
+
items = await work_items_service.search_work_items(
|
|
290
|
+
project_path="my-group/my-project", include_status=True
|
|
291
|
+
)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Status names are configured per namespace — match them case-insensitively.
|
|
295
|
+
|
|
209
296
|
## Bundled agent skills
|
|
210
297
|
|
|
211
298
|
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.
|
|
@@ -87,7 +87,7 @@ across_group = await work_items_service.search_work_items(
|
|
|
87
87
|
)
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
##
|
|
90
|
+
## Open/closed state
|
|
91
91
|
|
|
92
92
|
```python
|
|
93
93
|
from pjdev_gitlab.models import WorkItemStateEvent
|
|
@@ -100,6 +100,39 @@ await work_items_service.set_work_item_state(
|
|
|
100
100
|
)
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
+
## Custom status
|
|
104
|
+
|
|
105
|
+
Separate from open/closed: the workflow status an instance defines for itself
|
|
106
|
+
(`Ready`, `In progress`, `Code Complete`, ...). It is **not** a label, and the
|
|
107
|
+
REST API does not expose it at all — GraphQL's status widget is the only way to
|
|
108
|
+
read it.
|
|
109
|
+
|
|
110
|
+
Reading it is opt-in, because the widget is a GitLab *experiment* introduced in
|
|
111
|
+
**17.11**: selecting it against an older instance fails the entire query, so it
|
|
112
|
+
is never requested unless asked for.
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
# Just the status — one small query, no discussions dragged along.
|
|
116
|
+
# This is the right call on a hot path (e.g. per webhook event).
|
|
117
|
+
status = await work_items_service.get_work_item_status(42, project_path="group/proj")
|
|
118
|
+
print(status.name if status else "no status set")
|
|
119
|
+
|
|
120
|
+
# Or alongside everything else, when you were fetching the item anyway.
|
|
121
|
+
item = await work_items_service.get_work_item(
|
|
122
|
+
42, project_path="group/proj", include_status=True
|
|
123
|
+
)
|
|
124
|
+
print(item.status.name if item.status else "no status set")
|
|
125
|
+
|
|
126
|
+
# Also available on searches.
|
|
127
|
+
items = await work_items_service.search_work_items(
|
|
128
|
+
project_path="group/proj", include_status=True
|
|
129
|
+
)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`None` means the item has no status assigned. Status names are configured per
|
|
133
|
+
namespace, so **match them case-insensitively** rather than hard-coding an enum —
|
|
134
|
+
`"Code Complete"` on one instance may be `"code complete"` on another.
|
|
135
|
+
|
|
103
136
|
## Comments and threaded replies
|
|
104
137
|
|
|
105
138
|
```python
|
|
@@ -6,6 +6,7 @@ from typing import Any, Dict, List, Literal, Optional
|
|
|
6
6
|
from httpx import AsyncClient
|
|
7
7
|
from loguru import logger
|
|
8
8
|
|
|
9
|
+
from pjdev_gitlab import notes_service
|
|
9
10
|
from pjdev_gitlab.api_utilities import (
|
|
10
11
|
async_retry_http,
|
|
11
12
|
encode_path_segment,
|
|
@@ -227,6 +228,31 @@ async def comment_on_issue(
|
|
|
227
228
|
return await _exec(client)
|
|
228
229
|
|
|
229
230
|
|
|
231
|
+
async def list_issue_notes(
|
|
232
|
+
project_id: ProjectId,
|
|
233
|
+
issue_iid: int,
|
|
234
|
+
*,
|
|
235
|
+
include_system: bool = True,
|
|
236
|
+
page_size: int = 100,
|
|
237
|
+
client: Optional[AsyncClient] = None,
|
|
238
|
+
) -> List[Note]:
|
|
239
|
+
"""Fetch every note on an issue.
|
|
240
|
+
|
|
241
|
+
Thin alias for :func:`pjdev_gitlab.notes_service.list_notes`; see that module
|
|
242
|
+
for threads, single-note reads, and replying. Deliberately undecorated -- the
|
|
243
|
+
delegate already retries, and stacking the decorator would square the attempt
|
|
244
|
+
count.
|
|
245
|
+
"""
|
|
246
|
+
return await notes_service.list_notes(
|
|
247
|
+
project_id,
|
|
248
|
+
notes_service.Noteable.issue,
|
|
249
|
+
issue_iid,
|
|
250
|
+
include_system=include_system,
|
|
251
|
+
page_size=page_size,
|
|
252
|
+
client=client,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
230
256
|
@async_retry_http(status_codes_to_ignore=_IGNORE_4XX)
|
|
231
257
|
async def start_issue_discussion(
|
|
232
258
|
project_id: ProjectId,
|
|
@@ -2,6 +2,7 @@ from typing import Any, Dict, List, Optional
|
|
|
2
2
|
|
|
3
3
|
from httpx import AsyncClient
|
|
4
4
|
|
|
5
|
+
from pjdev_gitlab import notes_service
|
|
5
6
|
from pjdev_gitlab.api_utilities import (
|
|
6
7
|
async_retry_http,
|
|
7
8
|
encode_path_segment,
|
|
@@ -181,24 +182,29 @@ async def comment_on_merge_request_diff(
|
|
|
181
182
|
return await _exec(client)
|
|
182
183
|
|
|
183
184
|
|
|
184
|
-
@async_retry_http(status_codes_to_ignore=_IGNORE_4XX)
|
|
185
185
|
async def list_merge_request_notes(
|
|
186
186
|
project_id: ProjectId,
|
|
187
187
|
mr_iid: int,
|
|
188
188
|
*,
|
|
189
|
+
include_system: bool = True,
|
|
189
190
|
page_size: int = 100,
|
|
190
191
|
client: Optional[AsyncClient] = None,
|
|
191
192
|
) -> List[Note]:
|
|
192
|
-
|
|
193
|
+
"""Fetch every note on a merge request.
|
|
193
194
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
195
|
+
Thin alias for :func:`pjdev_gitlab.notes_service.list_notes`; see that module
|
|
196
|
+
for threads, single-note reads, and replying. Deliberately undecorated -- the
|
|
197
|
+
delegate already retries, and stacking the decorator would square the attempt
|
|
198
|
+
count.
|
|
199
|
+
"""
|
|
200
|
+
return await notes_service.list_notes(
|
|
201
|
+
project_id,
|
|
202
|
+
notes_service.Noteable.merge_request,
|
|
203
|
+
mr_iid,
|
|
204
|
+
include_system=include_system,
|
|
205
|
+
page_size=page_size,
|
|
206
|
+
client=client,
|
|
207
|
+
)
|
|
202
208
|
|
|
203
209
|
|
|
204
210
|
@async_retry_http(status_codes_to_ignore=_IGNORE_4XX)
|
|
@@ -265,6 +265,32 @@ class WorkItemIteration(GraphQLBase):
|
|
|
265
265
|
web_url: Optional[str] = None
|
|
266
266
|
|
|
267
267
|
|
|
268
|
+
class WorkItemStatus(GraphQLBase):
|
|
269
|
+
"""A custom work-item status (``Ready``, ``In progress``, ``Code Complete``, ...).
|
|
270
|
+
|
|
271
|
+
Distinct from :class:`WorkItemState`, which is only ``OPEN``/``CLOSED``.
|
|
272
|
+
Statuses are defined per namespace, so the names are whatever the instance's
|
|
273
|
+
administrators configured — match them case-insensitively rather than
|
|
274
|
+
hard-coding an enum.
|
|
275
|
+
|
|
276
|
+
GitLab exposes this as an **experiment** (``WorkItemWidgetStatus``,
|
|
277
|
+
introduced in 17.11), which is why the widget is opt-in throughout
|
|
278
|
+
:mod:`pjdev_gitlab.work_items_service` — see ``include_status``. ``category``
|
|
279
|
+
and ``description`` are 18.1+ and are deliberately not selected, so the
|
|
280
|
+
queries here stay valid against 17.11.
|
|
281
|
+
|
|
282
|
+
Every field is optional: an instance may return a status object before it has
|
|
283
|
+
been fully configured, and a parse failure here would take down the whole
|
|
284
|
+
work-item read.
|
|
285
|
+
"""
|
|
286
|
+
|
|
287
|
+
id: Optional[str] = None
|
|
288
|
+
name: Optional[str] = None
|
|
289
|
+
icon_name: Optional[str] = None
|
|
290
|
+
color: Optional[str] = None
|
|
291
|
+
position: Optional[int] = None
|
|
292
|
+
|
|
293
|
+
|
|
268
294
|
class WorkItemNote(GraphQLBase):
|
|
269
295
|
id: str
|
|
270
296
|
body: str
|
|
@@ -316,6 +342,9 @@ class WorkItem(GraphQLBase):
|
|
|
316
342
|
start_date: Optional[str] = None
|
|
317
343
|
due_date: Optional[str] = None
|
|
318
344
|
discussions: List[WorkItemDiscussion] = []
|
|
345
|
+
status: Optional[WorkItemStatus] = None
|
|
346
|
+
"""Only populated when the read requested ``include_status=True``; ``None``
|
|
347
|
+
otherwise, which is indistinguishable from "this item has no status set"."""
|
|
319
348
|
|
|
320
349
|
widgets: List[Dict[str, Any]] = []
|
|
321
350
|
|
|
@@ -345,6 +374,11 @@ class WorkItem(GraphQLBase):
|
|
|
345
374
|
elif widget_type == "START_AND_DUE_DATE":
|
|
346
375
|
flat["start_date"] = widget.get("startDate")
|
|
347
376
|
flat["due_date"] = widget.get("dueDate")
|
|
377
|
+
elif widget_type == "STATUS":
|
|
378
|
+
# Only present when the query opted in via include_status; a work
|
|
379
|
+
# item with no status assigned still sends the widget, with a
|
|
380
|
+
# null status.
|
|
381
|
+
flat["status"] = widget.get("status")
|
|
348
382
|
elif widget_type == "NOTES":
|
|
349
383
|
nodes = (widget.get("discussions") or {}).get("nodes") or []
|
|
350
384
|
flat["discussions"] = [
|