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.
Files changed (39) hide show
  1. {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/PKG-INFO +90 -4
  2. pjdev_gitlab-5.1.10/README.md +187 -0
  3. {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/pyproject.toml +3 -0
  4. pjdev_gitlab-5.1.10/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_work_items/SKILL.md +173 -0
  5. {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/__about__.py +1 -1
  6. {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/__init__.py +3 -0
  7. {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/api_utilities.py +154 -2
  8. {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/config_service.py +3 -0
  9. pjdev_gitlab-5.1.10/src/pjdev_gitlab/models.py +369 -0
  10. pjdev_gitlab-5.1.10/src/pjdev_gitlab/oauth_cli.py +122 -0
  11. pjdev_gitlab-5.1.10/src/pjdev_gitlab/oauth_service.py +354 -0
  12. pjdev_gitlab-5.1.10/src/pjdev_gitlab/work_items_service.py +720 -0
  13. pjdev_gitlab-5.1.10/test_report_pjdev-gitlab.txt +74 -0
  14. {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/tests/tests_for_api_utilities.py +33 -0
  15. pjdev_gitlab-5.1.10/tests/tests_for_oauth_cli.py +85 -0
  16. pjdev_gitlab-5.1.10/tests/tests_for_oauth_service.py +142 -0
  17. pjdev_gitlab-5.1.10/tests/tests_for_work_items_service.py +681 -0
  18. pjdev_gitlab-5.1.7/README.md +0 -101
  19. pjdev_gitlab-5.1.7/src/pjdev_gitlab/models.py +0 -173
  20. pjdev_gitlab-5.1.7/test_report_pjdev-gitlab.txt +0 -36
  21. {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/.gitignore +0 -0
  22. {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/LICENSE.txt +0 -0
  23. {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_issues/SKILL.md +0 -0
  24. {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_merge_requests/SKILL.md +0 -0
  25. {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_packages/SKILL.md +0 -0
  26. {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_repo_files/SKILL.md +0 -0
  27. {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/issues_service.py +0 -0
  28. {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/merge_requests_service.py +0 -0
  29. {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/packages_service.py +0 -0
  30. {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/py.typed +0 -0
  31. {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/repo_files_service.py +0 -0
  32. {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/test.sh +0 -0
  33. {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/tests/__init__.py +0 -0
  34. {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/tests/conftest.py +0 -0
  35. {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/tests/tests_for_issues_service.py +0 -0
  36. {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/tests/tests_for_merge_requests_service.py +0 -0
  37. {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/tests/tests_for_models.py +0 -0
  38. {pjdev_gitlab-5.1.7 → pjdev_gitlab-5.1.10}/tests/tests_for_packages_service.py +0 -0
  39. {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.7
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
  [![PyPI - Version](https://img.shields.io/pypi/v/pjdev-gitlab.svg)](https://pypi.org/project/pjdev-gitlab)
32
32
  [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pjdev-gitlab.svg)](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 | personal/group/project access token (`api` scope) |
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
+ [![PyPI - Version](https://img.shields.io/pypi/v/pjdev-gitlab.svg)](https://pypi.org/project/pjdev-gitlab)
4
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pjdev-gitlab.svg)](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.
@@ -27,6 +27,9 @@ dependencies = [
27
27
  "pydantic>=2.12.5",
28
28
  ]
29
29
 
30
+ [project.scripts]
31
+ pjdev-gitlab-auth = "pjdev_gitlab.oauth_cli:main"
32
+
30
33
  [project.optional-dependencies]
31
34
  dev = [
32
35
  "ruff",
@@ -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.
@@ -1,4 +1,4 @@
1
1
  # SPDX-FileCopyrightText: 2026-present Chris O'Neill <chris@purplejay.io>
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
- __version__ = "5.1.7"
4
+ __version__ = "5.1.10"
@@ -12,6 +12,9 @@ __all__ = [
12
12
  "issues_service",
13
13
  "merge_requests_service",
14
14
  "models",
15
+ "oauth_cli",
16
+ "oauth_service",
15
17
  "packages_service",
16
18
  "repo_files_service",
19
+ "work_items_service",
17
20
  ]