Skip to main content

pjdev-gitlab

PyPI - Version PyPI - Python Version

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

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":

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:

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).

pjdev-gitlab-sync console script

pjdev-gitlab-sync clones a repo over HTTPS using an OAuth token, or fast-forwards it if it is already present — no SSH key setup required. It is deterministic and idempotent: the same command both installs a repo the first time and pulls updates on every later run, so it is the one thing a non-technical user has to remember.

pjdev-gitlab-sync developers/claude-skills \
  --gitlab-url https://gitlab.example.com \
  --client-id <client-id> \
  --target ~/git/claude-skills      # optional; defaults to the repo basename

It mints/reuses the token via the same PKCE flow as pjdev-gitlab-auth (opening a browser only when there is no valid cached token), then hands it to git through GIT_ASKPASS. The token is therefore never written to .git/config, placed on a git command line, or cached in a system keychain — the stored origin remote is always a plain, tokenless HTTPS URL you can safely inspect. The resolved checkout path is printed to stdout (so it is scriptable, e.g. cd "$(pjdev-gitlab-sync group/name)"); status goes to stderr.

Updates are merge --ff-only: if the checkout has diverging local commits, the command fails loudly rather than discarding your work. Flags mirror pjdev-gitlab-auth plus --target (GL_SYNC_TARGET) and --branch (GL_SYNC_BRANCH); the repo path may also come from GL_REPO.

Recommended: 1Password + op run

On a developer laptop, keep the token in 1Password and inject it into the host process with op run — 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:

    # .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:

    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

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:

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())

Custom statuses

The workflow status an instance defines for itself (Ready, In progress, ...) is neither a label nor the open/closed state, and REST does not expose it. Reading it is opt-in, because GitLab ships the status widget as an experiment (17.11+) and selecting it against an older instance fails the whole query:

# Just the status — one small query. Preferred on a hot path.
status = await work_items_service.get_work_item_status(
    42, project_path="my-group/my-project"
)
print(status.name if status else "no status set")

# Or folded into a read you were making anyway.
item = await work_items_service.get_work_item(
    42, project_path="my-group/my-project", include_status=True
)
items = await work_items_service.search_work_items(
    project_path="my-group/my-project", include_status=True
)

Status names are configured per namespace — match them case-insensitively.

Bundled agent skills

Skill files for AI agents ship under .agents/skills/ inside the installed package, following the library-skills.io convention. Topics: issues, workItems, merge requests, repository files, generic packages.

License

pjdev-gitlab is distributed under the terms of the MIT license.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pjdev_gitlab-5.1.13.tar.gz (54.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pjdev_gitlab-5.1.13-py3-none-any.whl (51.0 kB view details)

Uploaded Python 3

File details

Details for the file pjdev_gitlab-5.1.13.tar.gz.

File metadata

  • Download URL: pjdev_gitlab-5.1.13.tar.gz
  • Upload date:
  • Size: 54.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: Hatch/1.17.1 {"ci":true,"cpu":"aarch64","distro":{"id":"noble","libc":{"lib":"glibc","version":"2.39"},"name":"Ubuntu","version":"24.04"},"implementation":{"name":"CPython","version":"3.12.3"},"installer":{"name":"hatch","version":"1.17.1"},"openssl_version":"OpenSSL 3.0.13 30 Jan 2024","python":"3.12.3","system":{"name":"Linux","release":"6.12.76-linuxkit"}} HTTPX2/2.9.1

File hashes

Hashes for pjdev_gitlab-5.1.13.tar.gz
Algorithm Hash digest
SHA256 0cff4c1b762dbd5265a175c432cc36f076c0fc91bb4599aacd8d1c2991059bc2
MD5 0a8e583c40d2b08d3c3c0d713188c5b0
BLAKE2b-256 f7c9b521bdeb17e048bdf1f1dd947309fafa98fbc2a633d4a421ae9c7d122672

See more details on using hashes here.

File details

Details for the file pjdev_gitlab-5.1.13-py3-none-any.whl.

File metadata

  • Download URL: pjdev_gitlab-5.1.13-py3-none-any.whl
  • Upload date:
  • Size: 51.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: Hatch/1.17.1 {"ci":true,"cpu":"aarch64","distro":{"id":"noble","libc":{"lib":"glibc","version":"2.39"},"name":"Ubuntu","version":"24.04"},"implementation":{"name":"CPython","version":"3.12.3"},"installer":{"name":"hatch","version":"1.17.1"},"openssl_version":"OpenSSL 3.0.13 30 Jan 2024","python":"3.12.3","system":{"name":"Linux","release":"6.12.76-linuxkit"}} HTTPX2/2.9.1

File hashes

Hashes for pjdev_gitlab-5.1.13-py3-none-any.whl
Algorithm Hash digest
SHA256 fca951f775f96cf684a858dab25d3756864df0392d10669478b7906248ab3cb5
MD5 ea9a1952cae03b9ca835045541d7744b
BLAKE2b-256 3d20785ea3c60c01f4d797beaccf926285fca3e453e140d885221a1f18f7bb2a

See more details on using hashes here.

Release history Release notifications | RSS feed

5.4.0

2 files

5.3.0

2 files

5.2.0

2 files

5.1.14

2 files

This release

5.1.13 This release

2 files

5.1.12

2 files

5.1.10

2 files

5.1.9

2 files

5.1.7

2 files

5.1.6

2 files

5.1.5

2 files

5.1.4

2 files

5.1.2

2 files

5.1.1

2 files

5.1.0

2 files

5.0.3

1 file

5.0.2

2 files

5.0.1

2 files

5.0.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page