GitHub - qodevai/gitlab-api: Python client library for the GitLab API

CI PyPI

qodev-gitlab-api

A lightweight, typed Python client for the GitLab REST API. Built on httpx with automatic pagination, structured error handling, and .env support, it provides a clean interface for common GitLab operations without the weight of a full-featured SDK.

Why this library?

  • Lightweight -- just httpx, no heavy ORM-like abstractions
  • Typed -- ships with py.typed for full mypy/pyright support
  • Agent-friendly -- simple method signatures, dict returns, auto-pagination
  • Built for tools -- designed for MCP servers and CLIs, not full application frameworks
  • Focused -- python-gitlab is comprehensive but heavy; this library covers the operations AI agents and developer tools actually need

Installation

pip install qodev-gitlab-api

Quick Start

from qodev_gitlab_api import GitLabClient

# Reads GITLAB_TOKEN and GITLAB_URL from environment or .env file
client = GitLabClient()

# Or pass credentials explicitly
client = GitLabClient(token="glpat-xxxxxxxxxxxx", base_url="https://gitlab.example.com")

# Skip connection validation for faster startup
client = GitLabClient(validate=False)
# List open merge requests
mrs = client.get_merge_requests("mygroup/myproject", state="opened")
for mr in mrs:
    print(f"!{mr['iid']} {mr['title']}")

# Create a merge request
client.create_merge_request("mygroup/myproject", "feature-branch", "main", "Add new feature")

# Get pipeline status
pipelines = client.get_pipelines("mygroup/myproject")

Features

  • Merge requests -- create, update, merge, close, and review with inline diff comments
  • Pipelines and jobs -- list, inspect, wait for completion, retry, and download artifacts
  • Issues -- create, update, close, and comment
  • Releases -- create, update, delete, and list with asset links
  • CI/CD variables -- get, list, create, update, and upsert (set) project variables
  • File operations -- read repository files at any ref, upload files for markdown embedding
  • Automatic pagination -- all list endpoints handle multi-page results transparently
  • Typed exceptions -- AuthenticationError, NotFoundError, APIError, ConfigurationError

Configuration

The client reads configuration from environment variables, with .env file support via python-dotenv:

Variable Description Default
GITLAB_TOKEN GitLab personal access token (required) --
GITLAB_URL GitLab instance base URL https://gitlab.com

You can also use GITLAB_BASE_URL as an alias for GITLAB_URL.

Create a .env file in your project root:

GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx
GITLAB_URL=https://gitlab.example.com

API Reference

Projects

projects = client.get_projects(owned=True)
project = client.get_project("my-group/my-project")

Merge Requests

# List and inspect
mrs = client.get_merge_requests("my-group/my-project", state="opened")
mr = client.get_merge_request("my-group/my-project", mr_iid=42)
changes = client.get_mr_changes("my-group/my-project", mr_iid=42)
commits = client.get_mr_commits("my-group/my-project", mr_iid=42)
approvals = client.get_mr_approvals("my-group/my-project", mr_iid=42)

# Create
mr = client.create_merge_request(
    "my-group/my-project",
    source_branch="feature/foo",
    target_branch="main",
    title="Add foo feature",
    description="Implements the foo feature.",
    assignee_ids=[123],
    reviewer_ids=[456],
    labels="enhancement",
)

# Update, merge, close
client.update_mr("my-group/my-project", mr_iid=42, title="Updated title")
client.merge_mr("my-group/my-project", mr_iid=42, squash=True)
client.close_mr("my-group/my-project", mr_iid=42)

# Discussions and comments
discussions = client.get_mr_discussions("my-group/my-project", mr_iid=42)
client.create_mr_note("my-group/my-project", mr_iid=42, body="Looks good!")
client.reply_to_discussion("my-group/my-project", mr_iid=42, discussion_id="abc123", body="Fixed.")
client.resolve_discussion("my-group/my-project", mr_iid=42, discussion_id="abc123", resolved=True)

# Inline diff comment
from qodev_gitlab_api import DiffPosition

client.create_mr_discussion(
    "my-group/my-project",
    mr_iid=42,
    body="Consider renaming this variable.",
    position=DiffPosition(file_path="src/main.py", new_line=15),
)

Pipelines and Jobs

pipelines = client.get_pipelines("my-group/my-project", ref="main")
pipeline = client.get_pipeline("my-group/my-project", pipeline_id=1001)
jobs = client.get_pipeline_jobs("my-group/my-project", pipeline_id=1001)

# Job details, logs, and artifacts
job = client.get_job("my-group/my-project", job_id=5001)
log = client.get_job_log("my-group/my-project", job_id=5001)
artifact = client.get_job_artifact("my-group/my-project", job_id=5001, artifact_path="report.xml")

# Retry a failed job
client.retry_job("my-group/my-project", job_id=5001)

# Wait for pipeline completion (blocks until done or timeout)
result = client.wait_for_pipeline("my-group/my-project", pipeline_id=1001, timeout_seconds=600)
print(result["final_status"])  # "success", "failed", "canceled", "skipped", or "timeout"

Issues

issues = client.get_issues("my-group/my-project", state="opened", labels="bug")
issue = client.get_issue("my-group/my-project", issue_iid=10)

issue = client.create_issue(
    "my-group/my-project",
    title="Fix login bug",
    description="Users cannot log in with SSO.",
    labels="bug,urgent",
    assignee_ids=[123],
)

client.update_issue("my-group/my-project", issue_iid=10, labels="bug,resolved")
client.close_issue("my-group/my-project", issue_iid=10)

# Comments
notes = client.get_issue_notes("my-group/my-project", issue_iid=10)
client.create_issue_note("my-group/my-project", issue_iid=10, body="Investigating this now.")

Releases

releases = client.get_releases("my-group/my-project")
release = client.get_release("my-group/my-project", tag_name="v1.0.0")

release = client.create_release(
    "my-group/my-project",
    tag_name="v1.1.0",
    name="Version 1.1.0",
    description="## What's new\n- Feature A\n- Bug fix B",
    ref="main",
)

client.update_release("my-group/my-project", tag_name="v1.1.0", description="Updated notes.")
client.delete_release("my-group/my-project", tag_name="v1.1.0")

CI/CD Variables

variables = client.list_project_variables("my-group/my-project")
var = client.get_project_variable("my-group/my-project", key="API_KEY")

# Create or update (upsert)
var, action = client.set_project_variable(
    "my-group/my-project",
    key="API_KEY",
    value="secret-value",
    masked=True,
    protected=True,
)
print(action)  # "created" or "updated"

File Operations

# Read a file from the repository
content = client.get_file_content("my-group/my-project", file_path="README.md", ref="main")

# Upload a file (for embedding in issues/MRs)
from qodev_gitlab_api import FileFromPath

result = client.upload_file("my-group/my-project", source=FileFromPath(path="/tmp/screenshot.png"))

Error Handling

All API errors raise typed exceptions that inherit from GitLabError:

from qodev_gitlab_api import GitLabClient, AuthenticationError, NotFoundError, APIError, ConfigurationError

try:
    client = GitLabClient()
    mr = client.get_merge_request("my-group/my-project", mr_iid=999)
except ConfigurationError:
    print("Missing or invalid GITLAB_TOKEN / GITLAB_URL")
except AuthenticationError:
    print("Token is invalid or expired")
except NotFoundError:
    print("Merge request not found")
except APIError as e:
    print(f"API error {e.status_code}: {e.response_body}")

Requirements

License

MIT -- see LICENSE for details.