Metadata-Version: 2.4
Name: pjdev-gitlab
Version: 5.1.10
Project-URL: Documentation, https://gitlab.purplejay.io/keystone/python/-/tree/main/pjdev-gitlab/README.md
Project-URL: Issues, https://gitlab.purplejay.io/keystone/python/-/issues
Project-URL: Source, https://gitlab.purplejay.io/keystone/python
Author-email: Purple Jay LLC <developers@purplejay.io>
License-Expression: MIT
License-File: LICENSE.txt
Classifier: Development Status :: 4 - Beta
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Requires-Python: >=3.12
Requires-Dist: httpx
Requires-Dist: loguru
Requires-Dist: pydantic-settings>=2.13.1
Requires-Dist: pydantic>=2.12.5
Provides-Extra: dev
Requires-Dist: ruff; extra == 'dev'
Provides-Extra: test
Requires-Dist: coverage; extra == 'test'
Requires-Dist: pytest; extra == 'test'
Requires-Dist: pytest-asyncio; extra == 'test'
Requires-Dist: respx; extra == 'test'
Description-Content-Type: text/markdown

# pjdev-gitlab

[![PyPI - Version](https://img.shields.io/pypi/v/pjdev-gitlab.svg)](https://pypi.org/project/pjdev-gitlab)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pjdev-gitlab.svg)](https://pypi.org/project/pjdev-gitlab)

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.

-----

## Installation

```console
pip install pjdev-gitlab
```

## Configuration

`pjdev-gitlab` reads `GL_*` environment variables (or accepts the same values
via `init()`):

| variable | required | purpose |
| --- | --- | --- |
| `GL_TOKEN` | yes | access token (`api` scope) — a PAT, or an OAuth token with `GL_AUTH_SCHEME=bearer` |
| `GL_GITLAB_URL` | yes | base URL, e.g. `https://gitlab.com` |
| `GL_AUTH_SCHEME` | no | `private-token` (default, PAT header) or `bearer` (OAuth access token) |
| `GL_DEFAULT_PROJECT_ID` | no | default project for service helpers |
| `GL_OUTPUT_PATH` | no | directory for downloaded files |

### OAuth 2.0 (per-user attribution)

To attribute API writes to a real person instead of a shared bot token, mint a
short-lived OAuth access token with the interactive Authorization Code + PKCE
flow and initialize with `auth_scheme="bearer"`:

```python
from pjdev_gitlab import config_service, oauth_service

# Opens a browser for sign-in/consent on first run; caches + refreshes the token
# per host under ~/.config/pjdev-gitlab/tokens/ (0600) on subsequent runs.
token = oauth_service.get_access_token(
    gitlab_url="https://gitlab.example.com",
    client_id="<registered PKCE app client id>",
)
config_service.init(
    token=token, gitlab_url="https://gitlab.example.com", auth_scheme="bearer"
)
```

The OAuth application must be registered as a non-confidential (PKCE) app with
**scope** `api` and **Redirect URI** exactly `http://localhost:7331/callback`
(override the port via `get_access_token(redirect_port=...)`). A `bearer` token
is sent as `Authorization: Bearer <token>`; the default `private-token` scheme
sends `PRIVATE-TOKEN` for personal/project/group access tokens.

#### `pjdev-gitlab-auth` console script

The same flow is exposed as a console script, so shell callers (e.g. skills) can
mint a token without embedding any Python. It prints only the token to stdout and
status to stderr:

```bash
TOKEN="$(uvx --from 'pjdev-gitlab' pjdev-gitlab-auth \
  --gitlab-url https://gitlab.example.com --client-id <client-id>)"
```

The library is instance-agnostic: `--client-id` (or `GL_OAUTH_CLIENT_ID`) is
required and the host defaults to `https://gitlab.com` (override with
`--gitlab-url` / `GL_GITLAB_URL`). Other flags: `--client-secret`,
`--redirect-port`, `--scopes`, `--force` (each with a `GL_OAUTH_*` env
equivalent).

### Recommended: 1Password + `op run`

On a developer laptop, keep the token in 1Password and inject it into the host
process with [`op run`](https://developer.1password.com/docs/cli/secrets-environment-variables/) —
the secret never sits in your shell environment or on disk in plaintext.

1. Install the 1Password CLI (`brew install --cask 1password-cli`) and turn on
   **Settings → Developer → Integrate with 1Password CLI** in the desktop app.
2. Store the token in 1Password (e.g. an API Credential titled
   `GitLab — purplejay` with a `credential` field).
3. Drop a committable `.env.op` next to your project — references only, no
   real secrets:

   ```bash
   # .env.op
   GL_TOKEN="op://Private/GitLab — purplejay/credential"
   GL_GITLAB_URL="https://gitlab.purplejay.io"
   ```

4. Launch your script — or the entire Claude Code session that will use this
   library — under `op run`:

   ```bash
   op run --env-file=.env.op -- python my_script.py
   op run --env-file=.env.op -- claude
   ```

`op run` resolves the references, exports them to the subprocess, and tears
them down on exit. Inside Python, just call `config_service.init()` with no
arguments and the values flow in from the environment.

In CI, skip 1Password and set `GL_TOKEN`/`GL_GITLAB_URL` from the job's
existing variables (e.g. `CI_JOB_TOKEN` for project-scoped operations).

## Usage

```python
import asyncio
from pjdev_gitlab import config_service, issues_service
from pjdev_gitlab.models import StateEvent

async def main() -> None:
    # Token & URL come from GL_TOKEN / GL_GITLAB_URL (e.g. via `op run`).
    config_service.init(default_project_id="my-group/my-project")

    issue = await issues_service.create_issue(
        project_id="my-group/my-project",
        title="Bug: timeout on /widgets",
        description="The endpoint times out under load.\n\n/label ~bug ~priority::high",
        labels=["bug"],
    )
    await issues_service.comment_on_issue(
        project_id="my-group/my-project",
        issue_iid=issue.iid,
        body="Investigating now.",
    )
    await issues_service.set_issue_state(
        project_id="my-group/my-project",
        issue_iid=issue.iid,
        state_event=StateEvent.close,
    )

asyncio.run(main())
```

Run it: `op run --env-file=.env.op -- python my_script.py`.

### WorkItems (GraphQL)

`workItem` is GitLab's unified replacement for the legacy `Issue` type — use
`work_items_service` for status/comments/labels on modern GitLab instances:

```python
import asyncio
from pjdev_gitlab import config_service, work_items_service
from pjdev_gitlab.models import WorkItemState, WorkItemStateEvent

async def main() -> None:
    config_service.init()

    open_bugs = await work_items_service.search_work_items(
        project_path="my-group/my-project",
        state=WorkItemState.OPEN,
        labels=["bug"],
        search="timeout",
    )

    label = await work_items_service.create_label(
        "needs-review", project_path="my-group/my-project", color="#FFAA00"
    )
    await work_items_service.set_work_item_labels(
        open_bugs[0].iid,
        [label.id],
        mode="add",
        project_path="my-group/my-project",
    )
    await work_items_service.comment_on_work_item(
        open_bugs[0].iid,
        "Triaged — assigning a reviewer.",
        project_path="my-group/my-project",
    )
    await work_items_service.set_work_item_state(
        open_bugs[0].iid, WorkItemStateEvent.CLOSE,
        project_path="my-group/my-project",
    )

asyncio.run(main())
```

## Bundled agent skills

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.

## License

`pjdev-gitlab` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
