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.
- Lightweight -- just
httpx, no heavy ORM-like abstractions - Typed -- ships with
py.typedfor 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-gitlabis comprehensive but heavy; this library covers the operations AI agents and developer tools actually need
pip install qodev-gitlab-apifrom 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")- 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
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
projects = client.get_projects(owned=True)
project = client.get_project("my-group/my-project")# 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 = 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 = 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 = 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")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"# 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"))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}")- Python 3.11+
- httpx >= 0.28.1
- python-dotenv >= 1.1.0
MIT -- see LICENSE for details.