pjdev-gitlab 5.4.0__tar.gz → 2026.1007.82101__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.4.0 → pjdev_gitlab-2026.1007.82101}/.gitignore +3 -1
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/PKG-INFO +46 -3
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/README.md +45 -2
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_work_items/SKILL.md +74 -1
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/src/pjdev_gitlab/__about__.py +1 -1
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/src/pjdev_gitlab/__init__.py +1 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/src/pjdev_gitlab/api_utilities.py +11 -0
- pjdev_gitlab-2026.1007.82101/src/pjdev_gitlab/custom_fields_service.py +723 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/src/pjdev_gitlab/models.py +101 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/test_report_pjdev-gitlab.txt +158 -130
- pjdev_gitlab-2026.1007.82101/tests/tests_for_custom_fields_service.py +588 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/LICENSE.txt +0 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/pyproject.toml +0 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_issues/SKILL.md +0 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_merge_requests/SKILL.md +0 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_packages/SKILL.md +0 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_repo_files/SKILL.md +0 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/src/pjdev_gitlab/config_service.py +0 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/src/pjdev_gitlab/git_sync_service.py +0 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/src/pjdev_gitlab/issues_service.py +0 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/src/pjdev_gitlab/merge_requests_service.py +0 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/src/pjdev_gitlab/notes_service.py +0 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/src/pjdev_gitlab/oauth_cli.py +0 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/src/pjdev_gitlab/oauth_service.py +0 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/src/pjdev_gitlab/packages_service.py +0 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/src/pjdev_gitlab/py.typed +0 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/src/pjdev_gitlab/repo_files_service.py +0 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/src/pjdev_gitlab/sync_cli.py +0 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/src/pjdev_gitlab/users_service.py +0 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/src/pjdev_gitlab/work_items_service.py +0 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/test.sh +0 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/tests/__init__.py +0 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/tests/conftest.py +0 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/tests/tests_for_api_utilities.py +0 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/tests/tests_for_git_sync_service.py +0 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/tests/tests_for_issues_service.py +0 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/tests/tests_for_merge_requests_service.py +0 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/tests/tests_for_models.py +0 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/tests/tests_for_notes_service.py +0 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/tests/tests_for_oauth_cli.py +0 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/tests/tests_for_oauth_service.py +0 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/tests/tests_for_packages_service.py +0 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/tests/tests_for_repo_files_service.py +0 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/tests/tests_for_sync_cli.py +0 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/tests/tests_for_users_service.py +0 -0
- {pjdev_gitlab-5.4.0 → pjdev_gitlab-2026.1007.82101}/tests/tests_for_work_items_service.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pjdev-gitlab
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2026.1007.82101
|
|
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 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.
|
|
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, custom fields), merge requests, repository files, and the generic package registry.
|
|
35
35
|
|
|
36
36
|
-----
|
|
37
37
|
|
|
@@ -383,9 +383,52 @@ items = await work_items_service.search_work_items(
|
|
|
383
383
|
|
|
384
384
|
Status names are configured per namespace — match them case-insensitively.
|
|
385
385
|
|
|
386
|
+
#### Custom fields
|
|
387
|
+
|
|
388
|
+
Custom fields are typed properties (single/multi select, number, text, date)
|
|
389
|
+
defined on a **top-level group** and inherited by every subgroup and project
|
|
390
|
+
under it (Premium/Ultimate, GitLab 18.0+). `custom_fields_service` accepts
|
|
391
|
+
either `group_path` or `project_path` everywhere and resolves it to that group:
|
|
392
|
+
|
|
393
|
+
```python
|
|
394
|
+
from pjdev_gitlab import custom_fields_service
|
|
395
|
+
from pjdev_gitlab.models import CustomFieldType
|
|
396
|
+
|
|
397
|
+
# Define once — lands on "my-group", the project's top-level group.
|
|
398
|
+
team = await custom_fields_service.create_custom_field(
|
|
399
|
+
"Team",
|
|
400
|
+
CustomFieldType.SINGLE_SELECT,
|
|
401
|
+
project_path="my-group/my-project",
|
|
402
|
+
select_options=["Platform", "Payments"],
|
|
403
|
+
work_item_types=["Issue", "Task"], # names, ids or Global IDs
|
|
404
|
+
)
|
|
405
|
+
await custom_fields_service.create_custom_field(
|
|
406
|
+
"Story points", CustomFieldType.NUMBER,
|
|
407
|
+
group_path="my-group", work_item_types=["Issue"],
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
# Set values on a work item: keyed by field name, plain Python values.
|
|
411
|
+
await custom_fields_service.set_work_item_custom_fields(
|
|
412
|
+
42,
|
|
413
|
+
{"Team": "Platform", "Story points": 5},
|
|
414
|
+
project_path="my-group/my-project",
|
|
415
|
+
)
|
|
416
|
+
values = await custom_fields_service.get_work_item_custom_field_values(
|
|
417
|
+
42, project_path="my-group/my-project"
|
|
418
|
+
)
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
Values are validated against the field definition before anything is sent;
|
|
422
|
+
`None` clears a field. Updating many items? Fetch the definitions once with
|
|
423
|
+
`list_custom_fields` and pass them as `fields=` to skip the per-call lookup.
|
|
424
|
+
`update_custom_field(select_options=...)` **replaces** the option list (dropped
|
|
425
|
+
options lose their selections) — use `add_custom_field_select_options` to
|
|
426
|
+
append. There is no delete: `archive_custom_field` hides a field and keeps its
|
|
427
|
+
values for `unarchive_custom_field`.
|
|
428
|
+
|
|
386
429
|
## Bundled agent skills
|
|
387
430
|
|
|
388
|
-
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.
|
|
431
|
+
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 (including custom fields), merge requests, repository files, generic packages.
|
|
389
432
|
|
|
390
433
|
## License
|
|
391
434
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://pypi.org/project/pjdev-gitlab)
|
|
4
4
|
[](https://pypi.org/project/pjdev-gitlab)
|
|
5
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.
|
|
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, custom fields), merge requests, repository files, and the generic package registry.
|
|
7
7
|
|
|
8
8
|
-----
|
|
9
9
|
|
|
@@ -355,9 +355,52 @@ items = await work_items_service.search_work_items(
|
|
|
355
355
|
|
|
356
356
|
Status names are configured per namespace — match them case-insensitively.
|
|
357
357
|
|
|
358
|
+
#### Custom fields
|
|
359
|
+
|
|
360
|
+
Custom fields are typed properties (single/multi select, number, text, date)
|
|
361
|
+
defined on a **top-level group** and inherited by every subgroup and project
|
|
362
|
+
under it (Premium/Ultimate, GitLab 18.0+). `custom_fields_service` accepts
|
|
363
|
+
either `group_path` or `project_path` everywhere and resolves it to that group:
|
|
364
|
+
|
|
365
|
+
```python
|
|
366
|
+
from pjdev_gitlab import custom_fields_service
|
|
367
|
+
from pjdev_gitlab.models import CustomFieldType
|
|
368
|
+
|
|
369
|
+
# Define once — lands on "my-group", the project's top-level group.
|
|
370
|
+
team = await custom_fields_service.create_custom_field(
|
|
371
|
+
"Team",
|
|
372
|
+
CustomFieldType.SINGLE_SELECT,
|
|
373
|
+
project_path="my-group/my-project",
|
|
374
|
+
select_options=["Platform", "Payments"],
|
|
375
|
+
work_item_types=["Issue", "Task"], # names, ids or Global IDs
|
|
376
|
+
)
|
|
377
|
+
await custom_fields_service.create_custom_field(
|
|
378
|
+
"Story points", CustomFieldType.NUMBER,
|
|
379
|
+
group_path="my-group", work_item_types=["Issue"],
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
# Set values on a work item: keyed by field name, plain Python values.
|
|
383
|
+
await custom_fields_service.set_work_item_custom_fields(
|
|
384
|
+
42,
|
|
385
|
+
{"Team": "Platform", "Story points": 5},
|
|
386
|
+
project_path="my-group/my-project",
|
|
387
|
+
)
|
|
388
|
+
values = await custom_fields_service.get_work_item_custom_field_values(
|
|
389
|
+
42, project_path="my-group/my-project"
|
|
390
|
+
)
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
Values are validated against the field definition before anything is sent;
|
|
394
|
+
`None` clears a field. Updating many items? Fetch the definitions once with
|
|
395
|
+
`list_custom_fields` and pass them as `fields=` to skip the per-call lookup.
|
|
396
|
+
`update_custom_field(select_options=...)` **replaces** the option list (dropped
|
|
397
|
+
options lose their selections) — use `add_custom_field_select_options` to
|
|
398
|
+
append. There is no delete: `archive_custom_field` hides a field and keeps its
|
|
399
|
+
values for `unarchive_custom_field`.
|
|
400
|
+
|
|
358
401
|
## Bundled agent skills
|
|
359
402
|
|
|
360
|
-
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.
|
|
403
|
+
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 (including custom fields), merge requests, repository files, generic packages.
|
|
361
404
|
|
|
362
405
|
## License
|
|
363
406
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
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,
|
|
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, managing labels (including creating new project or group labels), and defining/setting custom fields (typed work item properties). Trigger when the user asks to query, create, or modify GitLab workItems (or "issues" on a recent GitLab instance) programmatically.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# pjdev_gitlab_work_items
|
|
@@ -18,6 +18,7 @@ Reference docs (source of truth for behavior):
|
|
|
18
18
|
- [GraphQL reference](https://docs.gitlab.com/api/graphql/reference/)
|
|
19
19
|
- [WorkItems](https://docs.gitlab.com/user/work_items/)
|
|
20
20
|
- [Labels](https://docs.gitlab.com/user/project/labels/)
|
|
21
|
+
- [Custom fields](https://docs.gitlab.com/user/work_items/custom_fields/)
|
|
21
22
|
- [Comments and threads](https://docs.gitlab.com/user/discussions/)
|
|
22
23
|
|
|
23
24
|
## Setup
|
|
@@ -133,6 +134,78 @@ items = await work_items_service.search_work_items(
|
|
|
133
134
|
namespace, so **match them case-insensitively** rather than hard-coding an enum —
|
|
134
135
|
`"Code Complete"` on one instance may be `"code complete"` on another.
|
|
135
136
|
|
|
137
|
+
## Custom fields
|
|
138
|
+
|
|
139
|
+
Typed properties on work items — `SINGLE_SELECT`, `MULTI_SELECT`, `NUMBER`,
|
|
140
|
+
`TEXT`, `DATE` — via `custom_fields_service`. Premium/Ultimate, GitLab 18.0+.
|
|
141
|
+
|
|
142
|
+
Definitions live **only on the top-level group** and are inherited by every
|
|
143
|
+
subgroup and project below it. Every helper takes `group_path=` or
|
|
144
|
+
`project_path=` and resolves to that group, so you never need to work out the
|
|
145
|
+
root yourself. Each field is shown only on the work item types it is
|
|
146
|
+
associated with — create it without `work_item_types` and it appears nowhere.
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
import datetime
|
|
150
|
+
|
|
151
|
+
from pjdev_gitlab import custom_fields_service
|
|
152
|
+
from pjdev_gitlab.models import CustomFieldType
|
|
153
|
+
|
|
154
|
+
# What exists already (active only by default; active=None for archived too)
|
|
155
|
+
fields = await custom_fields_service.list_custom_fields(project_path="group/proj")
|
|
156
|
+
types = await custom_fields_service.list_work_item_types(project_path="group/proj")
|
|
157
|
+
|
|
158
|
+
# Define (Maintainer+ on the top-level group)
|
|
159
|
+
team = await custom_fields_service.create_custom_field(
|
|
160
|
+
"Team",
|
|
161
|
+
CustomFieldType.SINGLE_SELECT,
|
|
162
|
+
project_path="group/proj", # -> created on "group"
|
|
163
|
+
select_options=["Platform", "Payments"],
|
|
164
|
+
work_item_types=["Issue", "Task"], # names, numeric ids or Global IDs
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Grow a select field without disturbing existing selections
|
|
168
|
+
team = await custom_fields_service.add_custom_field_select_options(team, ["Mobile"])
|
|
169
|
+
|
|
170
|
+
# Set values: keys are field names (case-insensitive) or Global IDs
|
|
171
|
+
await custom_fields_service.set_work_item_custom_fields(
|
|
172
|
+
42,
|
|
173
|
+
{
|
|
174
|
+
"Team": "platform", # option value or option Global ID
|
|
175
|
+
"Components": ["API", "UI"], # multi-select replaces the selection
|
|
176
|
+
"Story points": 5,
|
|
177
|
+
"Customer": "Acme",
|
|
178
|
+
"Target date": datetime.date(2026, 11, 1),
|
|
179
|
+
"Old field": None, # None clears
|
|
180
|
+
},
|
|
181
|
+
project_path="group/proj",
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Read
|
|
185
|
+
for v in await custom_fields_service.get_work_item_custom_field_values(
|
|
186
|
+
42, project_path="group/proj"
|
|
187
|
+
):
|
|
188
|
+
print(v.custom_field.name, v.value, [o.value for o in v.selected_options])
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Gotchas:
|
|
192
|
+
|
|
193
|
+
- `set_work_item_custom_fields` validates every value against the field type
|
|
194
|
+
and its options **before** sending; an unknown field or option raises
|
|
195
|
+
`ValueError` (wrapped in an `ExceptionGroup` by the retry decorator) listing
|
|
196
|
+
what is valid. Fields you do not name are left untouched.
|
|
197
|
+
- GitLab answers "Invalid custom field ID" when the field is archived or not
|
|
198
|
+
associated with the item's work item type.
|
|
199
|
+
- Bulk updates: call `list_custom_fields` once and pass `fields=` — otherwise
|
|
200
|
+
every call re-lists the definitions.
|
|
201
|
+
- `update_custom_field(select_options=...)` **replaces** the whole option list:
|
|
202
|
+
pass existing `CustomFieldSelectOption` objects to keep them, strings to add.
|
|
203
|
+
Omitted options are deleted along with their selections on work items.
|
|
204
|
+
- No hard delete: `archive_custom_field` hides a field (values kept),
|
|
205
|
+
`unarchive_custom_field` restores it.
|
|
206
|
+
- Limits: 50 active fields per group, 10 per work item type, 50 options per
|
|
207
|
+
select field, 1024 characters per text value.
|
|
208
|
+
|
|
136
209
|
## Comments and threaded replies
|
|
137
210
|
|
|
138
211
|
```python
|
|
@@ -166,6 +166,7 @@ async def paginate_graphql(
|
|
|
166
166
|
*,
|
|
167
167
|
connection_path: List[str],
|
|
168
168
|
page_size: int = 100,
|
|
169
|
+
require_connection: bool = False,
|
|
169
170
|
client: Optional[httpx.AsyncClient] = None,
|
|
170
171
|
) -> List[Dict[str, Any]]:
|
|
171
172
|
"""Follow ``pageInfo.endCursor`` through a Relay-style GraphQL connection.
|
|
@@ -177,6 +178,10 @@ async def paginate_graphql(
|
|
|
177
178
|
object (e.g. ``["group", "workItems"]``).
|
|
178
179
|
|
|
179
180
|
The variable ``first`` is set to ``page_size`` on every page.
|
|
181
|
+
|
|
182
|
+
GraphQL answers an unknown or inaccessible ``fullPath`` with ``null`` rather
|
|
183
|
+
than an error, which reads as "no results". ``require_connection=True``
|
|
184
|
+
raises :class:`LookupError` instead when the connection is missing.
|
|
180
185
|
"""
|
|
181
186
|
|
|
182
187
|
def _walk(data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
@@ -195,6 +200,12 @@ async def paginate_graphql(
|
|
|
195
200
|
data = await graphql(query, page_vars, client=_client)
|
|
196
201
|
connection = _walk(data)
|
|
197
202
|
if connection is None:
|
|
203
|
+
if require_connection:
|
|
204
|
+
raise LookupError(
|
|
205
|
+
f"{'.'.join(connection_path)} not found: "
|
|
206
|
+
f"{variables.get('path', '<path>')!r} does not exist "
|
|
207
|
+
"or is not visible to this token"
|
|
208
|
+
)
|
|
198
209
|
break
|
|
199
210
|
nodes.extend(connection.get("nodes") or [])
|
|
200
211
|
page_info = connection.get("pageInfo") or {}
|