
Gitlab
- 25 installs
- 1.3k repo stars
- Updated July 27, 2026
- microsoft/hve-core
gitlab provides GitLab security workflow guidance from hve-core.
About
The gitlab skill from hve-core provides GitLab-oriented security and development workflow references for agents assisting with GitLab repositories, pipelines, and secure configuration within the hve-core skill collection.
- GitLab workflow and security references.
- Part of Microsoft hve-core skill pack.
- Secure GitLab configuration guidance.
Gitlab by the numbers
- 25 all-time installs (skills.sh)
- Ranked #1,549 of 2,209 Security skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
gitlab capabilities & compatibility
- Capabilities
- gitlab skill in hve core
- Use cases
- security audit
What gitlab says it does
gitlab
npx skills add https://github.com/microsoft/hve-core --skill gitlabAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 25 |
|---|---|
| repo stars | ★ 1.3k |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 27, 2026 |
| Repository | microsoft/hve-core ↗ |
What GitLab security guidance does hve-core provide?
GitLab security and workflow guidance from Microsoft hve-core skill pack.
Who is it for?
Teams using GitLab with hve-core skills.
Skip if: Skip for GitHub-only workflows without GitLab.
When should I use this skill?
GitLab security or workflow questions in hve-core context.
What you get
GitLab recommendations per hve-core references.
Files
GitLab Skill
Overview
Use this skill to inspect and update GitLab merge requests, notes, pipelines, and job logs against GitLab.com or self-managed GitLab instances.
This skill is the repository-local Python workflow for GitLab tasks. It is not the official GitLab MCP server integration surface.
This first hve-core implementation is Python-only. Run the CLI through python scripts/gitlab.py and prefer --fields for read operations to keep output concise.
Prerequisites
The skill requires Python 3.11 or later.
Set these environment variables before running any command:
| Variable | Required | Example | Purpose |
|---|---|---|---|
GITLAB_URL | Yes | https://gitlab.com | GitLab instance URL |
GITLAB_TOKEN | Yes | glpat-... | Personal access token sent as PRIVATE-TOKEN |
GITLAB_PROJECT | No | group/project | Project path or numeric project ID |
If GITLAB_PROJECT is not set, the script attempts to detect the project from git remote get-url origin. Set the variable explicitly when you are not in a git repository or when you want to target a different project.
Quick Start
Export your environment variables, then run a read command with --fields.
export GITLAB_URL="https://gitlab.com"
export GITLAB_TOKEN="glpat-..."
export GITLAB_PROJECT="group/project"
python scripts/gitlab.py mr-list opened --fields iid,title,author.nameRead pipeline jobs for a known pipeline:
python scripts/gitlab.py pipeline-jobs 12345 --fields id,name,status,stageParameters Reference
Common Option
| Parameter | Applies To | Example | Description |
|---|---|---|---|
--fields | mr-list, mr-get, mr-notes, pipeline-get, pipeline-jobs | --fields iid,title,state | Extract specific fields with dot notation and print concise tabular or key-value output |
Commands
| Command | Arguments | Description |
|---|---|---|
mr-list | [state] [max] | List merge requests, defaulting to all states and 20 results |
mr-get | <mr-iid> | Get one merge request by project-scoped IID |
mr-create | <json> or stdin | Create a merge request from a JSON payload |
mr-update | <mr-iid> <json> or stdin | Update merge request fields from a JSON payload |
mr-comment | <mr-iid> <body> or stdin | Add a comment to a merge request |
mr-notes | <mr-iid> [max] | List merge request notes, excluding system notes when using --fields |
pipeline-get | <pipeline-id> | Get one pipeline by numeric ID |
pipeline-run | <branch-or-tag> | Trigger a pipeline for a branch or tag |
pipeline-jobs | <pipeline-id> | List jobs for a pipeline |
job-log | <job-id> | Print raw log output for a job |
Script Reference
List recent open merge requests:
python scripts/gitlab.py mr-list opened --fields iid,title,author.name,user_notes_countGet one merge request:
python scripts/gitlab.py mr-get 42 --fields iid,title,state,source_branch,target_branchCreate a merge request from inline JSON:
python scripts/gitlab.py mr-create '{
"source_branch": "feature/add-auth",
"target_branch": "main",
"title": "feat(auth): add OAuth login"
}'Add a merge request comment from standard input:
echo "CI passed. Ready for review." | python scripts/gitlab.py mr-comment 42Inspect a failed pipeline:
python scripts/gitlab.py pipeline-get 12345 --fields id,status,web_url
python scripts/gitlab.py pipeline-jobs 12345 --fields id,name,status,stage
python scripts/gitlab.py job-log 67890Troubleshooting
| Symptom | Cause | Resolution |
|---|---|---|
GITLAB_URL is not set | Required environment variable missing | Export GITLAB_URL before running the script |
GITLAB_TOKEN is not set | Missing personal access token | Create a token with API access and export GITLAB_TOKEN |
cannot parse git remote URL | Project autodetection failed | Set GITLAB_PROJECT explicitly |
HTTP 401 or HTTP 403 | Token is invalid or lacks access | Verify token scope and project permissions |
HTTP 404 | Wrong project, MR IID, pipeline ID, or job ID | Verify GITLAB_PROJECT and confirm the numeric identifiers |
expected numeric ID | Non-numeric value passed to an ID argument | Use project MR IID values and numeric pipeline or job IDs |
python3 is required or syntax errors on launch | Unsupported interpreter | Run the script with Python 3.11 or later |
GitLab uses MR IIDs such as !42 inside a project. This skill expects the numeric IID, not the global merge request ID.
[project]
name = "gitlab-skill"
version = "0.0.0"
requires-python = ">=3.11"
dependencies = []
[dependency-groups]
dev = [
"pytest>=9.0",
"pytest-cov>=7.0",
"ruff>=0.15",
"pytest-mock>=3.12",
]
# Atheris ships manylinux-only wheels; keep separate from dev so uv sync works on macOS.
fuzz = [
"atheris>=3.0",
]
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["scripts"]
python_files = ["test_*.py", "fuzz_harness.py"]
[tool.ruff]
line-length = 88
target-version = "py311"
[tool.ruff.lint]
select = ["E", "F", "I", "W"]
[tool.pyright]
include = ["tests", "scripts"]
extraPaths = ["scripts"]
pythonVersion = "3.11"
venvPath = "."
venv = ".venv"
#!/usr/bin/env python3
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: MIT
# /// script
# requires-python = ">=3.11"
# ///
"""GitLab REST API v4 client for merge requests, pipelines, and jobs.
Environment variables:
GITLAB_URL: Required GitLab base URL.
GITLAB_TOKEN: Required personal access token.
GITLAB_PROJECT: Optional project id or path. Auto-detected from git remote.
"""
from __future__ import annotations
import json
import os
import re
import subprocess
import sys
import urllib.error
import urllib.parse
import urllib.request
from typing import Any, Callable, NoReturn, cast
EXIT_SUCCESS = 0
EXIT_FAILURE = 1
EXIT_USAGE = 2
selected_fields: list[str] | None = None
gitlab_url = ""
gitlab_token = ""
api_url = ""
sys.dont_write_bytecode = True
def die(message: str, exit_code: int = EXIT_FAILURE) -> NoReturn:
"""Print an error and raise SystemExit.
Args:
message: Error text to print.
exit_code: Process exit code.
Returns:
Never returns. The annotation is kept simple for CLI usage.
"""
print(f"error: {message}", file=sys.stderr)
raise SystemExit(exit_code)
def require_environment() -> None:
"""Load and validate required environment variables."""
global api_url
global gitlab_token
global gitlab_url
gitlab_url = os.environ.get("GITLAB_URL", "")
gitlab_token = os.environ.get("GITLAB_TOKEN", "")
if not gitlab_url:
die("GITLAB_URL is not set", EXIT_USAGE)
if not re.match(r"^https?://", gitlab_url):
die(
"GITLAB_URL must start with https:// (or http:// for local dev)",
EXIT_USAGE,
)
if not gitlab_token:
die("GITLAB_TOKEN is not set", EXIT_USAGE)
api_url = gitlab_url.rstrip("/") + "/api/v4"
def strip_git_suffix(path: str) -> str:
"""Remove a trailing .git suffix when present."""
if path.endswith(".git"):
return path[:-4]
return path
def project() -> str:
"""Resolve the target GitLab project from environment or git remote."""
configured_project = os.environ.get("GITLAB_PROJECT", "")
if configured_project:
return urllib.parse.quote(configured_project, safe="")
try:
remote_url = subprocess.check_output(
["git", "remote", "get-url", "origin"],
stderr=subprocess.DEVNULL,
text=True,
).strip()
except (subprocess.CalledProcessError, FileNotFoundError):
die("GITLAB_PROJECT not set and no git remote found", EXIT_USAGE)
if remote_url.startswith("git@"):
path = remote_url.split(":", 1)[1]
elif re.match(r"^https?://", remote_url):
path = re.sub(r"^https?://[^/]*/", "", remote_url)
else:
die(f"cannot parse git remote URL: {remote_url}", EXIT_USAGE)
path = strip_git_suffix(path)
if not path:
die(f"cannot extract project path from remote: {remote_url}", EXIT_USAGE)
return urllib.parse.quote(path, safe="")
def validate_numeric_id(value: str) -> None:
"""Validate that a CLI argument is a numeric identifier."""
if not re.match(r"^\d+$", value):
die(f"expected numeric ID, got: {value}", EXIT_USAGE)
def validate_positive_int(value: str, label: str = "value") -> None:
"""Validate that a CLI argument is a positive integer string."""
if not re.match(r"^\d+$", value):
die(f"{label} must be a positive integer, got: {value}", EXIT_USAGE)
def request(
method: str,
url: str,
data: object | None = None,
quiet: bool = False,
) -> object | None:
"""Issue an HTTP request to the GitLab API.
Args:
method: HTTP method.
url: Fully qualified request URL.
data: Optional JSON-serializable payload.
quiet: When True, suppress pretty-printed JSON output.
Returns:
Parsed JSON content, or None for empty or non-JSON responses.
"""
headers = {
"PRIVATE-TOKEN": gitlab_token,
"Content-Type": "application/json",
"Accept": "application/json",
}
body = json.dumps(data).encode() if data is not None else None
request_obj = urllib.request.Request(url, data=body, headers=headers, method=method)
try:
with urllib.request.urlopen(request_obj) as response:
raw = response.read().decode()
except urllib.error.HTTPError as error:
raw = error.read().decode()
try:
parsed_error = json.loads(raw)
print(
parsed_error.get("message", parsed_error.get("error", parsed_error)),
file=sys.stderr,
)
except (json.JSONDecodeError, ValueError):
print(raw, file=sys.stderr)
die(f"HTTP {error.code} from {method} {url}")
if not raw.strip():
return None
try:
parsed = json.loads(raw)
except (json.JSONDecodeError, ValueError):
print(raw)
return None
if not quiet:
print(json.dumps(parsed, indent=2))
return parsed
def parse_fields(arguments: list[str]) -> list[str]:
"""Extract the optional --fields argument from the CLI."""
global selected_fields
cleaned_arguments: list[str] = []
index = 0
while index < len(arguments):
current = arguments[index]
if current == "--fields":
if index + 1 >= len(arguments):
die("usage: --fields requires a comma-separated value list", EXIT_USAGE)
selected_fields = arguments[index + 1].split(",")
index += 2
continue
cleaned_arguments.append(current)
index += 1
return cleaned_arguments
def extract_field(obj: Any, path: str) -> str:
"""Extract a value using dot notation such as author.name."""
current = obj
for part in path.split("."):
if isinstance(current, dict):
current_dict = cast(dict[str, Any], current)
current = current_dict.get(part)
else:
return ""
if current is None:
return ""
if isinstance(current, list):
current_list = cast(list[Any], current)
return ", ".join(str(item) for item in current_list)
return str(cast(object, current))
def print_fields(data: Any) -> None:
"""Print extracted fields for a list response or a single object."""
if not selected_fields:
return
if isinstance(data, list):
print("\t".join(selected_fields))
for item in cast(list[Any], data):
print(
"\t".join(
extract_field(item, field_name) for field_name in selected_fields
)
)
return
for field_name in selected_fields:
print(f"{field_name}: {extract_field(data, field_name)}")
def load_json_payload(raw_payload: str, usage: str) -> object:
"""Parse a JSON payload or stop with a usage error."""
try:
return json.loads(raw_payload)
except json.JSONDecodeError as error:
die(f"invalid JSON payload: {error.msg}. {usage}", EXIT_USAGE)
def cmd_mr_list(args: list[str]) -> None:
"""List merge requests."""
state = args[0] if args else "all"
max_results = args[1] if len(args) > 1 else "20"
validate_positive_int(max_results, "max_results")
data = request(
"GET",
f"{api_url}/projects/{project()}/merge_requests?state={state}&per_page={max_results}&order_by=created_at&sort=desc",
quiet=bool(selected_fields),
)
if selected_fields and data is not None:
print_fields(data)
def cmd_mr_get(args: list[str]) -> None:
"""Get one merge request."""
if not args:
die("usage: gitlab mr-get <mr-iid>", EXIT_USAGE)
merge_request_iid = args[0]
validate_numeric_id(merge_request_iid)
data = request(
"GET",
f"{api_url}/projects/{project()}/merge_requests/{merge_request_iid}",
quiet=bool(selected_fields),
)
if selected_fields and data is not None:
print_fields(data)
def cmd_mr_create(args: list[str]) -> None:
"""Create a merge request from JSON input."""
raw_payload = args[0] if args else sys.stdin.read().strip()
usage = "usage: gitlab mr-create <json> or pipe JSON to stdin"
if not raw_payload:
die(usage, EXIT_USAGE)
request(
"POST",
f"{api_url}/projects/{project()}/merge_requests",
load_json_payload(raw_payload, usage),
)
def cmd_mr_update(args: list[str]) -> None:
"""Update a merge request from JSON input."""
if not args:
die("usage: gitlab mr-update <mr-iid> <json>", EXIT_USAGE)
merge_request_iid = args[0]
validate_numeric_id(merge_request_iid)
raw_payload = args[1] if len(args) > 1 else sys.stdin.read().strip()
usage = "usage: gitlab mr-update <mr-iid> <json> or pipe JSON to stdin"
if not raw_payload:
die(usage, EXIT_USAGE)
request(
"PUT",
f"{api_url}/projects/{project()}/merge_requests/{merge_request_iid}",
load_json_payload(raw_payload, usage),
)
def cmd_mr_comment(args: list[str]) -> None:
"""Create a merge request note."""
if not args:
die("usage: gitlab mr-comment <mr-iid> <body>", EXIT_USAGE)
merge_request_iid = args[0]
validate_numeric_id(merge_request_iid)
body = args[1] if len(args) > 1 else sys.stdin.read().strip()
if not body:
die(
"usage: gitlab mr-comment <mr-iid> <body> or pipe body to stdin",
EXIT_USAGE,
)
request(
"POST",
f"{api_url}/projects/{project()}/merge_requests/{merge_request_iid}/notes",
{"body": body},
)
def cmd_mr_notes(args: list[str]) -> None:
"""List merge request notes."""
if not args:
die("usage: gitlab mr-notes <mr-iid> [max]", EXIT_USAGE)
merge_request_iid = args[0]
validate_numeric_id(merge_request_iid)
max_results = args[1] if len(args) > 1 else "100"
validate_positive_int(max_results, "max_results")
data = request(
"GET",
f"{api_url}/projects/{project()}/merge_requests/{merge_request_iid}/notes?per_page={max_results}&sort=asc",
quiet=bool(selected_fields),
)
if selected_fields and isinstance(data, list):
notes = [
cast(dict[str, Any], note)
for note in cast(list[Any], data)
if isinstance(note, dict)
and not cast(dict[str, Any], note).get("system", False)
]
print_fields(notes)
def cmd_pipeline_get(args: list[str]) -> None:
"""Get one pipeline."""
if not args:
die("usage: gitlab pipeline-get <pipeline-id>", EXIT_USAGE)
pipeline_id = args[0]
validate_numeric_id(pipeline_id)
data = request(
"GET",
f"{api_url}/projects/{project()}/pipelines/{pipeline_id}",
quiet=bool(selected_fields),
)
if selected_fields and data is not None:
print_fields(data)
def cmd_pipeline_run(args: list[str]) -> None:
"""Trigger a pipeline for a branch or tag."""
if not args:
die("usage: gitlab pipeline-run <branch-or-tag>", EXIT_USAGE)
request("POST", f"{api_url}/projects/{project()}/pipelines", {"ref": args[0]})
def cmd_pipeline_jobs(args: list[str]) -> None:
"""List pipeline jobs."""
if not args:
die("usage: gitlab pipeline-jobs <pipeline-id>", EXIT_USAGE)
pipeline_id = args[0]
validate_numeric_id(pipeline_id)
data = request(
"GET",
f"{api_url}/projects/{project()}/pipelines/{pipeline_id}/jobs",
quiet=bool(selected_fields),
)
if selected_fields and data is not None:
print_fields(data)
def cmd_job_log(args: list[str]) -> None:
"""Print raw job trace output."""
if not args:
die("usage: gitlab job-log <job-id>", EXIT_USAGE)
job_id = args[0]
validate_numeric_id(job_id)
url = f"{api_url}/projects/{project()}/jobs/{job_id}/trace"
request_obj = urllib.request.Request(
url,
headers={"PRIVATE-TOKEN": gitlab_token},
method="GET",
)
try:
with urllib.request.urlopen(request_obj) as response:
print(response.read().decode())
except urllib.error.HTTPError as error:
print(error.read().decode(), file=sys.stderr)
die(f"HTTP {error.code} fetching job log")
COMMANDS: dict[str, Callable[[list[str]], None]] = {
"mr-list": cmd_mr_list,
"mr-get": cmd_mr_get,
"mr-create": cmd_mr_create,
"mr-update": cmd_mr_update,
"mr-comment": cmd_mr_comment,
"mr-notes": cmd_mr_notes,
"pipeline-get": cmd_pipeline_get,
"pipeline-run": cmd_pipeline_run,
"pipeline-jobs": cmd_pipeline_jobs,
"job-log": cmd_job_log,
}
def main() -> int:
"""Run the GitLab CLI."""
try:
arguments = parse_fields(sys.argv[1:])
require_environment()
if not arguments or arguments[0] not in COMMANDS:
die(
"usage: gitlab {mr-list|mr-get|mr-create|mr-update|mr-comment|"
"mr-notes|pipeline-get|pipeline-run|pipeline-jobs|job-log} "
"[args...]",
EXIT_USAGE,
)
COMMANDS[arguments[0]](arguments[1:])
return EXIT_SUCCESS
except KeyboardInterrupt:
print("Interrupted by user", file=sys.stderr)
return 130
except BrokenPipeError:
devnull_fd = os.open(os.devnull, os.O_WRONLY)
os.dup2(devnull_fd, sys.stdout.fileno())
os.close(devnull_fd)
return 141
if __name__ == "__main__":
sys.exit(main())
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: MIT
"""Shared fixtures for GitLab skill tests."""
from __future__ import annotations
import io
import urllib.error
from collections.abc import Callable
from dataclasses import dataclass, field
from email.message import Message
from types import ModuleType
from typing import Literal
import gitlab
import pytest
from test_constants import TEST_API_URL, TEST_GITLAB_TOKEN, TEST_GITLAB_URL
class FakeHttpResponse:
"""Minimal HTTP response stub for urllib tests."""
def __init__(self, body: str) -> None:
self._body = body.encode()
def __enter__(self) -> "FakeHttpResponse":
return self
def __exit__(self, exc_type: object, exc: object, tb: object) -> Literal[False]:
return False
def read(self) -> bytes:
return self._body
@dataclass
class RecordedCall:
"""Captured invocation of gitlab.request."""
method: str
url: str
data: object | None
quiet: bool
@dataclass
class RequestRecorder:
"""Callable test double that records request calls."""
response: object | None = None
calls: list[RecordedCall] = field(default_factory=list)
def __call__(
self,
method: str,
url: str,
data: object | None = None,
quiet: bool = False,
) -> object | None:
self.calls.append(RecordedCall(method=method, url=url, data=data, quiet=quiet))
return self.response
ConfiguredGitLab = ModuleType
ResponseFactory = Callable[[str], FakeHttpResponse]
StdinFactory = Callable[[str], None]
HttpErrorFactory = Callable[[str, int, str], urllib.error.HTTPError]
@pytest.fixture(autouse=True)
def reset_gitlab_state(monkeypatch: pytest.MonkeyPatch) -> None:
"""Reset module globals and seed environment variables for each test."""
gitlab.selected_fields = None
gitlab.gitlab_url = ""
gitlab.gitlab_token = ""
gitlab.api_url = ""
monkeypatch.setenv("GITLAB_URL", TEST_GITLAB_URL)
monkeypatch.setenv("GITLAB_TOKEN", TEST_GITLAB_TOKEN)
monkeypatch.delenv("GITLAB_PROJECT", raising=False)
@pytest.fixture
def configured_gitlab() -> ConfiguredGitLab:
"""Return the gitlab module with configured API globals."""
gitlab.gitlab_url = TEST_GITLAB_URL
gitlab.gitlab_token = TEST_GITLAB_TOKEN
gitlab.api_url = TEST_API_URL
return gitlab
@pytest.fixture
def http_error_factory() -> HttpErrorFactory:
"""Return a factory for urllib HTTPError objects with readable bodies."""
def _factory(
body: str, code: int = 400, url: str = TEST_API_URL
) -> urllib.error.HTTPError:
return urllib.error.HTTPError(
url=url,
code=code,
msg="error",
hdrs=Message(),
fp=io.BytesIO(body.encode()),
)
return _factory
@pytest.fixture
def response_factory() -> ResponseFactory:
"""Return a factory for minimal HTTP response stubs."""
def _factory(body: str) -> FakeHttpResponse:
return FakeHttpResponse(body)
return _factory
@pytest.fixture
def request_recorder() -> RequestRecorder:
"""Return a recording request double for command tests."""
return RequestRecorder()
@pytest.fixture
def stdin_factory(monkeypatch: pytest.MonkeyPatch) -> StdinFactory:
"""Return a helper that replaces stdin with text content."""
def _factory(text: str) -> None:
monkeypatch.setattr("sys.stdin", io.StringIO(text))
return _factory
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxrepogroup/project.gitリポ.gitrepo.git����3.1499999999999999-11234512345{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":"v"}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}author.name{"iid":42,"title":"test"}{"タイトル":"テスト"}{"title":"MR"}{"data":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}{"title":{}GARBAGE{"title":"MR","description":"fix"}3.1499999999999999-142①②③0--fields=id,title--fields=f0,f1,f2,f3,f4,f5,f6,f7,f8,f9,f10,f11,f12,f13,f14,f15,f16,f17,f18,f19,f20,f21,f22,f23,f24,f25,f26,f27,f28,f29,f30,f31,f32,f33,f34,f35,f36,f37,f38,f39,f40,f41,f42,f43,f44,f45,f46,f47,f48,f49,f50,f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61,f62,f63,f64,f65,f66,f67,f68,f69,f70,f71,f72,f73,f74,f75,f76,f77,f78,f79,f80,f81,f82,f83,f84,f85,f86,f87,f88,f89,f90,f91,f92,f93,f94,f95,f96,f97,f98,f99,f100,f101,f102,f103,f104,f105,f106,f107,f108,f109,f110,f111,f112,f113,f114,f115,f116,f117,f118,f119,f120,f121,f122,f123,f124,f125,f126,f127,f128,f129,f130,f131,f132,f133,f134,f135,f136,f137,f138,f139,f140,f141,f142,f143,f144,f145,f146,f147,f148,f149,f150,f151,f152,f153,f154,f155,f156,f157,f158,f159,f160,f161,f162,f163,f164,f165,f166,f167,f168,f169,f170,f171,f172,f173,f174,f175,f176,f177,f178,f179,f180,f181,f182,f183,f184,f185,f186,f187,f188,f189,f190,f191,f192,f193,f194,f195,f196,f197,f198,f199mr-list--fields=id,title,author--fields=id--fields=フィールド{"title":"MR"}<!-- markdownlint-disable-file -->
Fuzz Corpus Seeds
Seed inputs for the GitLab Atheris fuzz harness. Each file is raw bytes consumed by fuzz_dispatch which routes data[0] % 4 to one of four targets.
Naming Convention
{target_index}_{description} where target_index matches the FUZZ_TARGETS array position:
| Index | Target |
|---|---|
| 0 | fuzz_strip_git_suffix |
| 1 | fuzz_validate_numeric_id |
| 2 | fuzz_extract_field |
| 3 | fuzz_load_json_payload |
Usage
cd .github/skills/gitlab/gitlab
uv sync --group fuzz --group dev
uv run python tests/fuzz_harness.py tests/corpus/Atheris loads corpus files as starting inputs for coverage-guided mutation.
🤖 Crafted with precision by ✨Copilot following brilliant human instruction, then carefully refined by our team of discerning human reviewers.
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: MIT
"""Polyglot fuzz harness for GitLab skill helper logic.
Runs as a pytest test when Atheris is not installed.
Runs as an Atheris coverage-guided fuzz target when executed directly.
"""
from __future__ import annotations
import io
import sys
from contextlib import redirect_stderr, suppress
import gitlab
import pytest
try:
import atheris
except ImportError:
atheris = None
FUZZING = False
else:
FUZZING = True
def fuzz_strip_git_suffix(data: bytes) -> None:
"""Fuzz trimming of trailing .git suffixes."""
provider = atheris.FuzzedDataProvider(data)
value = provider.ConsumeUnicodeNoSurrogates(80)
gitlab.strip_git_suffix(value)
def fuzz_validate_numeric_id(data: bytes) -> None:
"""Fuzz numeric identifier validation."""
provider = atheris.FuzzedDataProvider(data)
value = provider.ConsumeUnicodeNoSurrogates(40)
with redirect_stderr(io.StringIO()), suppress(SystemExit):
gitlab.validate_numeric_id(value)
def fuzz_extract_field(data: bytes) -> None:
"""Fuzz nested field extraction on representative GitLab payloads."""
provider = atheris.FuzzedDataProvider(data)
payload = {
"iid": provider.ConsumeIntInRange(0, 500),
"author": {"name": provider.ConsumeUnicodeNoSurrogates(20)},
"labels": [provider.ConsumeUnicodeNoSurrogates(10) for _ in range(3)],
"nested": {"deep": {"value": provider.ConsumeIntInRange(0, 99)}},
}
path_options = [
"iid",
"author.name",
"labels",
"nested.deep.value",
provider.ConsumeUnicodeNoSurrogates(20),
]
gitlab.extract_field(
payload,
path_options[provider.ConsumeIntInRange(0, len(path_options) - 1)],
)
def fuzz_load_json_payload(data: bytes) -> None:
"""Fuzz JSON payload parsing."""
provider = atheris.FuzzedDataProvider(data)
raw_payload = provider.ConsumeUnicodeNoSurrogates(100)
with redirect_stderr(io.StringIO()), suppress(SystemExit):
gitlab.load_json_payload(raw_payload, "usage: gitlab")
def fuzz_validate_positive_int(data: bytes) -> None:
"""Fuzz validate_positive_int with arbitrary byte strings."""
fdp = atheris.FuzzedDataProvider(data)
text = fdp.ConsumeUnicodeNoSurrogates(fdp.remaining_bytes())
with redirect_stderr(io.StringIO()), suppress(SystemExit):
gitlab.validate_positive_int(text, "test-field")
def fuzz_parse_fields(data: bytes) -> None:
"""Fuzz parse_fields with arbitrary byte strings."""
fdp = atheris.FuzzedDataProvider(data)
count = fdp.ConsumeIntInRange(1, 6)
args = [
fdp.ConsumeUnicodeNoSurrogates(fdp.ConsumeIntInRange(0, 64))
for _ in range(count)
]
with redirect_stderr(io.StringIO()), suppress(SystemExit):
gitlab.parse_fields(args)
FUZZ_TARGETS = [
fuzz_strip_git_suffix,
fuzz_validate_numeric_id,
fuzz_extract_field,
fuzz_load_json_payload,
fuzz_validate_positive_int,
fuzz_parse_fields,
]
def fuzz_dispatch(data: bytes) -> None:
"""Route input to one fuzz target."""
if len(data) < 2:
return
target_index = data[0] % len(FUZZ_TARGETS)
FUZZ_TARGETS[target_index](data[1:])
class TestGitLabFuzzHarness:
"""Property tests mirroring fuzz-target behavior."""
@pytest.mark.parametrize(
("value", "expected"),
[
("group/project.git", "group/project"),
("group/project", "group/project"),
(".git", ""),
],
)
def test_strip_git_suffix(self, value: str, expected: str) -> None:
assert gitlab.strip_git_suffix(value) == expected
@pytest.mark.parametrize("value", ["7", "123456"])
def test_validate_numeric_id_accepts_digits(self, value: str) -> None:
gitlab.validate_numeric_id(value)
@pytest.mark.parametrize("value", ["", "abc", "12a", "-1"])
def test_validate_numeric_id_rejects_invalid_values(self, value: str) -> None:
with pytest.raises(SystemExit):
gitlab.validate_numeric_id(value)
def test_extract_field_handles_nested_values(self) -> None:
payload = {
"iid": 9,
"author": {"name": "Ada"},
"labels": ["bug", "urgent"],
}
assert gitlab.extract_field(payload, "iid") == "9"
assert gitlab.extract_field(payload, "author.name") == "Ada"
assert gitlab.extract_field(payload, "labels") == "bug, urgent"
@pytest.mark.parametrize(
("raw_payload", "expected"),
[
('{"title": "MR"}', {"title": "MR"}),
("[1, 2, 3]", [1, 2, 3]),
],
)
def test_load_json_payload(self, raw_payload: str, expected: object) -> None:
assert gitlab.load_json_payload(raw_payload, "usage: gitlab") == expected
if __name__ == "__main__" and FUZZING:
atheris.instrument_all()
atheris.Setup(sys.argv, fuzz_dispatch)
atheris.Fuzz()
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: MIT
"""Shared constants for GitLab skill tests."""
from __future__ import annotations
TEST_GITLAB_URL = "https://gitlab.example.com"
TEST_GITLAB_TOKEN = "test-token"
TEST_API_URL = f"{TEST_GITLAB_URL}/api/v4"
TEST_PROJECT = "group/project"
TEST_PROJECT_ENCODED = "group%2Fproject"
USAGE_MAIN = (
"usage: gitlab {mr-list|mr-get|mr-create|mr-update|mr-comment|mr-notes|"
"pipeline-get|pipeline-run|pipeline-jobs|job-log} [args...]"
)
USAGE_MR_GET = "usage: gitlab mr-get <mr-iid>"
USAGE_MR_CREATE = "usage: gitlab mr-create <json> or pipe JSON to stdin"
USAGE_MR_UPDATE = "usage: gitlab mr-update <mr-iid> <json> or pipe JSON to stdin"
USAGE_MR_COMMENT = "usage: gitlab mr-comment <mr-iid> <body> or pipe body to stdin"
USAGE_MR_NOTES = "usage: gitlab mr-notes <mr-iid> [max]"
USAGE_PIPELINE_GET = "usage: gitlab pipeline-get <pipeline-id>"
USAGE_PIPELINE_RUN = "usage: gitlab pipeline-run <branch-or-tag>"
USAGE_PIPELINE_JOBS = "usage: gitlab pipeline-jobs <pipeline-id>"
USAGE_JOB_LOG = "usage: gitlab job-log <job-id>"
FIELDS_MR = ["iid", "title"]
FIELDS_PIPELINE = ["id", "status"]
FIELDS_JOB = ["id", "name"]
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: MIT
"""Command-level tests for gitlab.py."""
from __future__ import annotations
from collections.abc import Callable
import gitlab
import pytest
from conftest import RequestRecorder, StdinFactory
from test_constants import (
FIELDS_JOB,
FIELDS_MR,
FIELDS_PIPELINE,
TEST_API_URL,
TEST_PROJECT_ENCODED,
USAGE_MR_COMMENT,
USAGE_MR_CREATE,
USAGE_MR_GET,
USAGE_MR_NOTES,
USAGE_MR_UPDATE,
USAGE_PIPELINE_GET,
USAGE_PIPELINE_JOBS,
USAGE_PIPELINE_RUN,
)
CommandFn = Callable[[list[str]], None]
MR_LIST_DEFAULT_URL = (
f"{TEST_API_URL}/projects/{TEST_PROJECT_ENCODED}/merge_requests?state=all&"
"per_page=20&order_by=created_at&sort=desc"
)
MR_GET_URL = f"{TEST_API_URL}/projects/{TEST_PROJECT_ENCODED}/merge_requests/42"
MR_CREATE_URL = f"{TEST_API_URL}/projects/{TEST_PROJECT_ENCODED}/merge_requests"
MR_UPDATE_URL = f"{TEST_API_URL}/projects/{TEST_PROJECT_ENCODED}/merge_requests/9"
MR_COMMENT_URL = (
f"{TEST_API_URL}/projects/{TEST_PROJECT_ENCODED}/merge_requests/5/notes"
)
MR_NOTES_URL = (
f"{TEST_API_URL}/projects/{TEST_PROJECT_ENCODED}/merge_requests/5/notes?"
"per_page=100&sort=asc"
)
PIPELINE_GET_URL = f"{TEST_API_URL}/projects/{TEST_PROJECT_ENCODED}/pipelines/10"
PIPELINE_RUN_URL = f"{TEST_API_URL}/projects/{TEST_PROJECT_ENCODED}/pipelines"
PIPELINE_JOBS_URL = f"{TEST_API_URL}/projects/{TEST_PROJECT_ENCODED}/pipelines/10/jobs"
MR_LIST_RESPONSE = [{"iid": 1, "title": "MR"}]
MR_GET_RESPONSE = {"iid": 42, "title": "MR"}
PIPELINE_RESPONSE = {"id": 10, "status": "success"}
PIPELINE_JOBS_RESPONSE = [{"id": 1, "name": "build"}]
FILTERED_NOTES = [
{"body": "human", "system": False},
{"body": "default-human"},
]
def _configure_command_test(
monkeypatch: pytest.MonkeyPatch,
request_recorder: RequestRecorder,
response: object | None = None,
) -> RequestRecorder:
gitlab.api_url = TEST_API_URL
request_recorder.response = response
monkeypatch.setattr(gitlab, "project", lambda: TEST_PROJECT_ENCODED)
monkeypatch.setattr(gitlab, "request", request_recorder)
return request_recorder
def _capture_print_fields(monkeypatch: pytest.MonkeyPatch) -> list[object]:
printed: list[object] = []
monkeypatch.setattr(gitlab, "print_fields", printed.append)
return printed
def _assert_usage_error(
command: CommandFn,
args: list[str],
expected_message: str,
capsys: pytest.CaptureFixture[str],
) -> None:
with pytest.raises(SystemExit) as exc_info:
command(args)
assert exc_info.value.code == gitlab.EXIT_USAGE
assert expected_message in capsys.readouterr().err
@pytest.mark.parametrize(
("command", "args", "expected_message"),
[
(gitlab.cmd_mr_get, [], USAGE_MR_GET),
(gitlab.cmd_mr_update, [], "usage: gitlab mr-update <mr-iid> <json>"),
(gitlab.cmd_mr_comment, [], "usage: gitlab mr-comment <mr-iid> <body>"),
(gitlab.cmd_mr_notes, [], USAGE_MR_NOTES),
(gitlab.cmd_pipeline_get, [], USAGE_PIPELINE_GET),
(gitlab.cmd_pipeline_run, [], USAGE_PIPELINE_RUN),
(gitlab.cmd_pipeline_jobs, [], USAGE_PIPELINE_JOBS),
],
)
def test_commands_require_minimum_arguments(
command: CommandFn,
args: list[str],
expected_message: str,
capsys: pytest.CaptureFixture[str],
) -> None:
_assert_usage_error(command, args, expected_message, capsys)
@pytest.mark.parametrize(
("command", "args", "expected_url"),
[
(gitlab.cmd_mr_get, ["42"], MR_GET_URL),
(gitlab.cmd_pipeline_get, ["10"], PIPELINE_GET_URL),
(gitlab.cmd_pipeline_jobs, ["10"], PIPELINE_JOBS_URL),
],
)
def test_get_commands_build_expected_urls(
monkeypatch: pytest.MonkeyPatch,
request_recorder: RequestRecorder,
command: CommandFn,
args: list[str],
expected_url: str,
) -> None:
recorder = _configure_command_test(monkeypatch, request_recorder, response={})
command(args)
assert recorder.calls[0].method == "GET"
assert recorder.calls[0].url == expected_url
assert recorder.calls[0].quiet is False
def test_mr_list_uses_default_state_and_page_size(
monkeypatch: pytest.MonkeyPatch,
request_recorder: RequestRecorder,
) -> None:
recorder = _configure_command_test(monkeypatch, request_recorder, response=[])
gitlab.cmd_mr_list([])
assert recorder.calls[0].method == "GET"
assert recorder.calls[0].url == MR_LIST_DEFAULT_URL
assert recorder.calls[0].quiet is False
@pytest.mark.parametrize(
("command", "args", "selected_fields", "response", "expected_printed"),
[
(
gitlab.cmd_mr_list,
["opened", "5"],
FIELDS_MR,
MR_LIST_RESPONSE,
MR_LIST_RESPONSE,
),
(gitlab.cmd_mr_get, ["42"], FIELDS_MR, MR_GET_RESPONSE, MR_GET_RESPONSE),
(
gitlab.cmd_pipeline_get,
["10"],
FIELDS_PIPELINE,
PIPELINE_RESPONSE,
PIPELINE_RESPONSE,
),
(
gitlab.cmd_pipeline_jobs,
["10"],
FIELDS_JOB,
PIPELINE_JOBS_RESPONSE,
PIPELINE_JOBS_RESPONSE,
),
],
)
def test_read_commands_print_selected_fields(
monkeypatch: pytest.MonkeyPatch,
request_recorder: RequestRecorder,
command: CommandFn,
args: list[str],
selected_fields: list[str],
response: object,
expected_printed: object,
) -> None:
gitlab.selected_fields = selected_fields
recorder = _configure_command_test(monkeypatch, request_recorder, response=response)
printed = _capture_print_fields(monkeypatch)
command(args)
assert recorder.calls[0].quiet is True
assert printed == [expected_printed]
@pytest.mark.parametrize(
("command", "args", "expected_url", "expected_data"),
[
(
gitlab.cmd_mr_create,
['{"title": "New MR"}'],
MR_CREATE_URL,
{"title": "New MR"},
),
(
gitlab.cmd_mr_update,
["9", '{"title": "Updated"}'],
MR_UPDATE_URL,
{"title": "Updated"},
),
(
gitlab.cmd_mr_comment,
["5", "Looks good"],
MR_COMMENT_URL,
{"body": "Looks good"},
),
(gitlab.cmd_pipeline_run, ["main"], PIPELINE_RUN_URL, {"ref": "main"}),
],
)
def test_write_commands_forward_inline_payloads(
monkeypatch: pytest.MonkeyPatch,
request_recorder: RequestRecorder,
command: CommandFn,
args: list[str],
expected_url: str,
expected_data: object,
) -> None:
recorder = _configure_command_test(monkeypatch, request_recorder)
command(args)
assert recorder.calls[0].url == expected_url
assert recorder.calls[0].data == expected_data
@pytest.mark.parametrize(
("command", "args", "stdin_text", "expected_url", "expected_data"),
[
(
gitlab.cmd_mr_create,
[],
'{"title": "stdin MR"}',
MR_CREATE_URL,
{"title": "stdin MR"},
),
(
gitlab.cmd_mr_update,
["9"],
'{"description": "from stdin"}',
MR_UPDATE_URL,
{"description": "from stdin"},
),
(
gitlab.cmd_mr_comment,
["5"],
"Ready for review",
MR_COMMENT_URL,
{"body": "Ready for review"},
),
],
)
def test_write_commands_read_payloads_from_stdin(
monkeypatch: pytest.MonkeyPatch,
request_recorder: RequestRecorder,
stdin_factory: StdinFactory,
command: CommandFn,
args: list[str],
stdin_text: str,
expected_url: str,
expected_data: object,
) -> None:
recorder = _configure_command_test(monkeypatch, request_recorder)
stdin_factory(stdin_text)
command(args)
assert recorder.calls[0].url == expected_url
assert recorder.calls[0].data == expected_data
@pytest.mark.parametrize(
("command", "args", "usage_message"),
[
(gitlab.cmd_mr_create, [], USAGE_MR_CREATE),
(gitlab.cmd_mr_update, ["9"], USAGE_MR_UPDATE),
(gitlab.cmd_mr_comment, ["5"], USAGE_MR_COMMENT),
],
)
def test_write_commands_require_stdin_or_inline_content(
stdin_factory: StdinFactory,
command: CommandFn,
args: list[str],
usage_message: str,
capsys: pytest.CaptureFixture[str],
) -> None:
stdin_factory("")
_assert_usage_error(command, args, usage_message, capsys)
def test_mr_notes_uses_default_max_results(
monkeypatch: pytest.MonkeyPatch,
request_recorder: RequestRecorder,
) -> None:
recorder = _configure_command_test(monkeypatch, request_recorder, response=[])
gitlab.cmd_mr_notes(["5"])
assert recorder.calls[0].url == MR_NOTES_URL
def test_mr_notes_filters_system_notes_before_printing(
monkeypatch: pytest.MonkeyPatch,
request_recorder: RequestRecorder,
) -> None:
gitlab.selected_fields = ["body"]
recorder = _configure_command_test(
monkeypatch,
request_recorder,
response=[
{"body": "human", "system": False},
{"body": "system", "system": True},
{"body": "default-human"},
],
)
printed = _capture_print_fields(monkeypatch)
gitlab.cmd_mr_notes(["5", "2"])
assert recorder.calls[0].quiet is True
assert printed == [FILTERED_NOTES]
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: MIT
"""Helper-oriented unit tests for gitlab.py."""
from __future__ import annotations
import gitlab
import pytest
class TestDie:
"""Tests for die."""
def test_prints_error_and_exits(self, capsys: pytest.CaptureFixture[str]) -> None:
with pytest.raises(SystemExit) as exc_info:
gitlab.die("boom", gitlab.EXIT_USAGE)
assert exc_info.value.code == gitlab.EXIT_USAGE
assert capsys.readouterr().err.strip() == "error: boom"
class TestStripGitSuffix:
"""Tests for strip_git_suffix."""
@pytest.mark.parametrize(
("value", "expected"),
[
("group/project.git", "group/project"),
("group/project", "group/project"),
(".git", ""),
("project.git.git", "project.git"),
],
)
def test_strips_expected_suffix(self, value: str, expected: str) -> None:
assert gitlab.strip_git_suffix(value) == expected
class TestValidateNumericId:
"""Tests for validate_numeric_id."""
@pytest.mark.parametrize("value", ["0", "7", "123456"])
def test_accepts_numeric_strings(self, value: str) -> None:
gitlab.validate_numeric_id(value)
@pytest.mark.parametrize("value", ["", "abc", "12a", "-1", "1.2", " 5 "])
def test_rejects_non_numeric_values(
self,
value: str,
capsys: pytest.CaptureFixture[str],
) -> None:
with pytest.raises(SystemExit) as exc_info:
gitlab.validate_numeric_id(value)
assert exc_info.value.code == gitlab.EXIT_USAGE
assert f"expected numeric ID, got: {value}" in capsys.readouterr().err
class TestValidatePositiveInt:
"""Tests for validate_positive_int."""
@pytest.mark.parametrize("value", ["0", "1", "250"])
def test_accepts_digit_strings(self, value: str) -> None:
gitlab.validate_positive_int(value, "max_results")
@pytest.mark.parametrize("value", ["", "ten", "5x", "-2", "3.14"])
def test_rejects_invalid_values(
self,
value: str,
capsys: pytest.CaptureFixture[str],
) -> None:
with pytest.raises(SystemExit) as exc_info:
gitlab.validate_positive_int(value, "max_results")
assert exc_info.value.code == gitlab.EXIT_USAGE
assert (
f"max_results must be a positive integer, got: {value}"
in capsys.readouterr().err
)
class TestParseFields:
"""Tests for parse_fields."""
def test_returns_arguments_without_fields(self) -> None:
arguments = ["mr-list", "opened", "20"]
cleaned = gitlab.parse_fields(arguments)
assert cleaned == arguments
assert gitlab.selected_fields is None
def test_extracts_fields_and_strips_option(self) -> None:
cleaned = gitlab.parse_fields(
["mr-list", "opened", "--fields", "iid,title,author.name"]
)
assert cleaned == ["mr-list", "opened"]
assert gitlab.selected_fields == ["iid", "title", "author.name"]
def test_fields_can_appear_before_command_arguments(self) -> None:
cleaned = gitlab.parse_fields(["--fields", "iid,title", "mr-get", "7"])
assert cleaned == ["mr-get", "7"]
assert gitlab.selected_fields == ["iid", "title"]
def test_requires_value_after_fields(
self, capsys: pytest.CaptureFixture[str]
) -> None:
with pytest.raises(SystemExit) as exc_info:
gitlab.parse_fields(["mr-list", "--fields"])
assert exc_info.value.code == gitlab.EXIT_USAGE
assert (
"usage: --fields requires a comma-separated value list"
in capsys.readouterr().err
)
class TestExtractField:
"""Tests for extract_field."""
@pytest.mark.parametrize(
("payload", "path", "expected"),
[
({"iid": 7}, "iid", "7"),
({"author": {"name": "Ada"}}, "author.name", "Ada"),
({"labels": ["bug", "urgent"]}, "labels", "bug, urgent"),
({"author": None}, "author.name", ""),
({"author": {"name": None}}, "author.name", ""),
({"author": {"name": "Ada"}}, "author.email", ""),
({"nested": {"deep": {"value": 9}}}, "nested.deep.value", "9"),
],
)
def test_extracts_supported_values(
self, payload: object, path: str, expected: str
) -> None:
assert gitlab.extract_field(payload, path) == expected
def test_returns_empty_for_non_mapping_intermediate_value(self) -> None:
assert gitlab.extract_field({"author": "Ada"}, "author.name") == ""
class TestPrintFields:
"""Tests for print_fields."""
def test_does_nothing_when_no_fields_selected(
self, capsys: pytest.CaptureFixture[str]
) -> None:
gitlab.print_fields({"iid": 7})
assert capsys.readouterr().out == ""
def test_prints_tabular_output_for_lists(
self, capsys: pytest.CaptureFixture[str]
) -> None:
gitlab.selected_fields = ["iid", "title"]
gitlab.print_fields(
[
{"iid": 1, "title": "First"},
{"iid": 2, "title": "Second"},
]
)
assert capsys.readouterr().out.splitlines() == [
"iid\ttitle",
"1\tFirst",
"2\tSecond",
]
def test_prints_key_value_output_for_single_object(
self, capsys: pytest.CaptureFixture[str]
) -> None:
gitlab.selected_fields = ["iid", "author.name"]
gitlab.print_fields({"iid": 9, "author": {"name": "Grace"}})
assert capsys.readouterr().out.splitlines() == [
"iid: 9",
"author.name: Grace",
]
class TestLoadJsonPayload:
"""Tests for load_json_payload."""
@pytest.mark.parametrize(
("raw_payload", "expected"),
[
('{"title": "MR"}', {"title": "MR"}),
("[1, 2, 3]", [1, 2, 3]),
("true", True),
],
)
def test_parses_valid_json(self, raw_payload: str, expected: object) -> None:
assert gitlab.load_json_payload(raw_payload, "usage: gitlab") == expected
def test_raises_usage_error_for_invalid_json(
self, capsys: pytest.CaptureFixture[str]
) -> None:
with pytest.raises(SystemExit) as exc_info:
gitlab.load_json_payload("{bad json}", "usage: gitlab mr-create <json>")
assert exc_info.value.code == gitlab.EXIT_USAGE
assert "invalid JSON payload" in capsys.readouterr().err
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: MIT
"""Entry-point tests for gitlab.py."""
from __future__ import annotations
import gitlab
import pytest
from test_constants import FIELDS_MR, USAGE_MAIN
ARGV_MAIN_LIST = ["gitlab", "mr-list", "opened", "5"]
ARGV_MAIN_FIELDS = ["gitlab", "mr-get", "42", "--fields", "iid,title,author.name"]
ARGV_FIELDS_ONLY = ["gitlab", "--fields", "iid,title"]
EXPECTED_FIELD_SELECTION = [*FIELDS_MR, "author.name"]
class TestMain:
"""Tests for main."""
def test_dispatches_to_selected_command(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
seen: list[object] = []
def fake_require_environment() -> None:
seen.append("env")
def fake_handler(args: list[str]) -> None:
seen.append(("handler", args))
monkeypatch.setattr(gitlab, "require_environment", fake_require_environment)
monkeypatch.setitem(gitlab.COMMANDS, "mr-list", fake_handler)
monkeypatch.setattr("sys.argv", ARGV_MAIN_LIST)
result = gitlab.main()
assert result == gitlab.EXIT_SUCCESS
assert seen == ["env", ("handler", ["opened", "5"])]
def test_main_applies_fields_before_dispatch(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
captured: list[object] = []
monkeypatch.setattr(
gitlab, "require_environment", lambda: captured.append("env")
)
def fake_handler(args: list[str]) -> None:
captured.append((args, gitlab.selected_fields))
monkeypatch.setitem(gitlab.COMMANDS, "mr-get", fake_handler)
monkeypatch.setattr("sys.argv", ARGV_MAIN_FIELDS)
result = gitlab.main()
assert result == gitlab.EXIT_SUCCESS
assert captured == ["env", (["42"], EXPECTED_FIELD_SELECTION)]
@pytest.mark.parametrize("argv", [["gitlab"], ["gitlab", "unknown-command"]])
def test_main_rejects_missing_or_unknown_command(
self,
monkeypatch: pytest.MonkeyPatch,
argv: list[str],
capsys: pytest.CaptureFixture[str],
) -> None:
monkeypatch.setattr(gitlab, "require_environment", lambda: None)
monkeypatch.setattr("sys.argv", argv)
with pytest.raises(SystemExit) as exc_info:
gitlab.main()
assert exc_info.value.code == gitlab.EXIT_USAGE
assert USAGE_MAIN in capsys.readouterr().err
def test_main_passes_empty_arguments_when_only_fields_are_present(
self,
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
monkeypatch.setattr(gitlab, "require_environment", lambda: None)
monkeypatch.setattr("sys.argv", ARGV_FIELDS_ONLY)
with pytest.raises(SystemExit) as exc_info:
gitlab.main()
assert exc_info.value.code == gitlab.EXIT_USAGE
assert gitlab.selected_fields == FIELDS_MR
assert USAGE_MAIN in capsys.readouterr().err
def test_main_handles_keyboard_interrupt(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""main returns 130 when KeyboardInterrupt is raised."""
monkeypatch.setattr(
gitlab,
"parse_fields",
lambda _: (_ for _ in ()).throw(KeyboardInterrupt),
)
assert gitlab.main() == 130
def test_main_handles_broken_pipe(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""main returns 141 and redirects stdout on BrokenPipeError."""
monkeypatch.setattr(
gitlab,
"parse_fields",
lambda _: (_ for _ in ()).throw(BrokenPipeError),
)
dup2_calls: list[tuple[int, int]] = []
close_calls: list[int] = []
monkeypatch.setattr("os.dup2", lambda fd, fd2: dup2_calls.append((fd, fd2)))
monkeypatch.setattr("os.open", lambda *a, **kw: 99)
monkeypatch.setattr("os.close", lambda fd: close_calls.append(fd))
assert gitlab.main() == 141
assert len(dup2_calls) == 1
assert close_calls == [99]
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: MIT
"""Transport and environment tests for gitlab.py."""
from __future__ import annotations
import json
import urllib.request
from typing import cast
import gitlab
import pytest
from conftest import ConfiguredGitLab, HttpErrorFactory, ResponseFactory
from pytest_mock import MockerFixture
from test_constants import (
TEST_API_URL,
TEST_GITLAB_TOKEN,
TEST_GITLAB_URL,
USAGE_JOB_LOG,
)
REQUEST_ENDPOINT = f"{TEST_API_URL}/test"
REQUEST_JSON = {"iid": 7, "title": "MR"}
REQUEST_BODY = '{"iid": 7, "title": "MR"}'
NON_JSON_BODY = "plain text output"
PROJECT_NOT_FOUND = "GITLAB_PROJECT not set and no git remote found"
PARSE_REMOTE_ERROR = "cannot parse git remote URL"
EMPTY_REMOTE_PATH_ERROR = "cannot extract project path from remote"
TRACE_UNAVAILABLE = "trace unavailable"
def _request_headers(request: urllib.request.Request) -> dict[str, str]:
return {key.lower(): value for key, value in request.header_items()}
class TestRequireEnvironment:
"""Tests for require_environment."""
def test_loads_environment_and_sets_api_url(self) -> None:
gitlab.require_environment()
assert gitlab.gitlab_url == TEST_GITLAB_URL
assert gitlab.gitlab_token == TEST_GITLAB_TOKEN
assert gitlab.api_url == TEST_API_URL
@pytest.mark.parametrize(
("env_name", "env_value", "expected_message"),
[
("GITLAB_URL", "", "GITLAB_URL is not set"),
("GITLAB_URL", "gitlab.example.com", "GITLAB_URL must start with https://"),
("GITLAB_TOKEN", "", "GITLAB_TOKEN is not set"),
],
)
def test_rejects_invalid_environment(
self,
monkeypatch: pytest.MonkeyPatch,
env_name: str,
env_value: str,
expected_message: str,
capsys: pytest.CaptureFixture[str],
) -> None:
monkeypatch.setenv(env_name, env_value)
with pytest.raises(SystemExit) as exc_info:
gitlab.require_environment()
assert exc_info.value.code == gitlab.EXIT_USAGE
assert expected_message in capsys.readouterr().err
class TestProject:
"""Tests for project."""
def test_prefers_explicit_project_environment(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("GITLAB_PROJECT", "group/project name")
assert gitlab.project() == "group%2Fproject%20name"
@pytest.mark.parametrize(
("remote_url", "expected"),
[
("git@gitlab.com:group/project.git\n", "group%2Fproject"),
("https://gitlab.com/group/project.git\n", "group%2Fproject"),
("http://gitlab.local/group/sub/project\n", "group%2Fsub%2Fproject"),
],
)
def test_parses_supported_remote_urls(
self,
mocker: MockerFixture,
remote_url: str,
expected: str,
) -> None:
mocker.patch("subprocess.check_output", return_value=remote_url)
assert gitlab.project() == expected
def test_requires_remote_when_project_not_configured(
self, mocker: MockerFixture, capsys: pytest.CaptureFixture[str]
) -> None:
mocker.patch("subprocess.check_output", side_effect=FileNotFoundError)
with pytest.raises(SystemExit) as exc_info:
gitlab.project()
assert exc_info.value.code == gitlab.EXIT_USAGE
assert PROJECT_NOT_FOUND in capsys.readouterr().err
def test_rejects_unparseable_remote(
self, mocker: MockerFixture, capsys: pytest.CaptureFixture[str]
) -> None:
mocker.patch(
"subprocess.check_output",
return_value="ssh://gitlab.example.com/group/project.git\n",
)
with pytest.raises(SystemExit) as exc_info:
gitlab.project()
assert exc_info.value.code == gitlab.EXIT_USAGE
assert PARSE_REMOTE_ERROR in capsys.readouterr().err
def test_rejects_empty_path_after_host(
self, mocker: MockerFixture, capsys: pytest.CaptureFixture[str]
) -> None:
mocker.patch(
"subprocess.check_output", return_value="https://gitlab.example.com/.git\n"
)
with pytest.raises(SystemExit) as exc_info:
gitlab.project()
assert exc_info.value.code == gitlab.EXIT_USAGE
assert EMPTY_REMOTE_PATH_ERROR in capsys.readouterr().err
class TestRequest:
"""Tests for request."""
def test_returns_parsed_json_and_prints_pretty_output(
self,
configured_gitlab: ConfiguredGitLab,
response_factory: ResponseFactory,
capsys: pytest.CaptureFixture[str],
mocker: MockerFixture,
) -> None:
captured_request: dict[str, urllib.request.Request] = {}
def fake_urlopen(request: urllib.request.Request) -> object:
captured_request["request"] = request
return response_factory(REQUEST_BODY)
mocker.patch("urllib.request.urlopen", side_effect=fake_urlopen)
parsed = gitlab.request("POST", REQUEST_ENDPOINT, {"title": "MR"})
assert parsed == REQUEST_JSON
request = cast(urllib.request.Request, captured_request["request"])
request_data = cast(bytes, request.data)
assert request.full_url == REQUEST_ENDPOINT
assert request.get_method() == "POST"
assert json.loads(request_data.decode()) == {"title": "MR"}
assert _request_headers(request)["private-token"] == TEST_GITLAB_TOKEN
assert '"iid": 7' in capsys.readouterr().out
def test_suppresses_output_when_quiet(
self,
configured_gitlab: ConfiguredGitLab,
response_factory: ResponseFactory,
capsys: pytest.CaptureFixture[str],
mocker: MockerFixture,
) -> None:
mocker.patch(
"urllib.request.urlopen",
return_value=response_factory('{"iid": 7}'),
)
parsed = gitlab.request("GET", REQUEST_ENDPOINT, quiet=True)
assert parsed == {"iid": 7}
assert capsys.readouterr().out == ""
def test_returns_none_for_empty_body(
self,
configured_gitlab: ConfiguredGitLab,
response_factory: ResponseFactory,
mocker: MockerFixture,
) -> None:
mocker.patch("urllib.request.urlopen", return_value=response_factory(" "))
assert gitlab.request("GET", REQUEST_ENDPOINT) is None
def test_prints_raw_text_for_non_json_response(
self,
configured_gitlab: ConfiguredGitLab,
response_factory: ResponseFactory,
capsys: pytest.CaptureFixture[str],
mocker: MockerFixture,
) -> None:
mocker.patch(
"urllib.request.urlopen",
return_value=response_factory(NON_JSON_BODY),
)
parsed = gitlab.request("GET", REQUEST_ENDPOINT)
assert parsed is None
assert capsys.readouterr().out.strip() == NON_JSON_BODY
def test_reports_structured_http_error(
self,
configured_gitlab: ConfiguredGitLab,
http_error_factory: HttpErrorFactory,
capsys: pytest.CaptureFixture[str],
mocker: MockerFixture,
) -> None:
error = http_error_factory('{"message": "forbidden"}', code=403)
mocker.patch("urllib.request.urlopen", side_effect=error)
with pytest.raises(SystemExit) as exc_info:
gitlab.request("GET", REQUEST_ENDPOINT)
assert exc_info.value.code == gitlab.EXIT_FAILURE
error_lines = capsys.readouterr().err.splitlines()
assert "forbidden" in error_lines[0]
assert f"error: HTTP 403 from GET {REQUEST_ENDPOINT}" in error_lines[1]
def test_reports_raw_http_error_body(
self,
configured_gitlab: ConfiguredGitLab,
http_error_factory: HttpErrorFactory,
capsys: pytest.CaptureFixture[str],
mocker: MockerFixture,
) -> None:
error = http_error_factory("Service unavailable", code=503)
mocker.patch("urllib.request.urlopen", side_effect=error)
with pytest.raises(SystemExit):
gitlab.request("DELETE", REQUEST_ENDPOINT)
error_lines = capsys.readouterr().err.splitlines()
assert error_lines[0] == "Service unavailable"
assert error_lines[1] == f"error: HTTP 503 from DELETE {REQUEST_ENDPOINT}"
class TestCmdJobLog:
"""Tests for cmd_job_log."""
def test_prints_job_log(
self,
configured_gitlab: ConfiguredGitLab,
response_factory: ResponseFactory,
capsys: pytest.CaptureFixture[str],
mocker: MockerFixture,
) -> None:
mocker.patch(
"urllib.request.urlopen",
return_value=response_factory("line one\nline two"),
)
gitlab.cmd_job_log(["99"])
assert capsys.readouterr().out.strip().splitlines() == ["line one", "line two"]
def test_requires_job_id(self, capsys: pytest.CaptureFixture[str]) -> None:
with pytest.raises(SystemExit) as exc_info:
gitlab.cmd_job_log([])
assert exc_info.value.code == gitlab.EXIT_USAGE
assert USAGE_JOB_LOG in capsys.readouterr().err
def test_rejects_non_numeric_job_id(
self, capsys: pytest.CaptureFixture[str]
) -> None:
with pytest.raises(SystemExit):
gitlab.cmd_job_log(["abc"])
assert "expected numeric ID, got: abc" in capsys.readouterr().err
def test_reports_http_error_for_log_fetch(
self,
configured_gitlab: ConfiguredGitLab,
http_error_factory: HttpErrorFactory,
capsys: pytest.CaptureFixture[str],
mocker: MockerFixture,
) -> None:
error = http_error_factory(TRACE_UNAVAILABLE, code=404)
mocker.patch("urllib.request.urlopen", side_effect=error)
with pytest.raises(SystemExit) as exc_info:
gitlab.cmd_job_log(["99"])
assert exc_info.value.code == gitlab.EXIT_FAILURE
error_lines = capsys.readouterr().err.splitlines()
assert error_lines[0] == TRACE_UNAVAILABLE
assert error_lines[1] == "error: HTTP 404 fetching job log"
version = 1
revision = 3
requires-python = ">=3.11"
[[package]]
name = "atheris"
version = "3.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f8/58/5965955898e16bee17c8379eae12194993bf641c4629016991248b862069/atheris-3.0.0.tar.gz", hash = "sha256:1f0929c7bc3040f3fe4102e557718734190cf2d7718bbb8e3ce6d3eb56ef5bb3", size = 373239, upload-time = "2025-11-24T23:54:02.15Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/da/15/cf109e2e8696a54c8c4bc3ef79a79bec32361eceb64eaa36690a682e83a9/atheris-3.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8a5c8a781467c187da40fd29139784193e2647058831f837f675d0bb8cbd8746", size = 34805555, upload-time = "2025-11-24T23:53:53.477Z" },
{ url = "https://files.pythonhosted.org/packages/85/8c/e9960b996e70e5f6a523670431166b2b238de52fef094955515dcf854da1/atheris-3.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:510e502c57b6dc615fb174066407af620d4c7f73cf08a782c86e7761bf12c4eb", size = 34907016, upload-time = "2025-11-24T23:53:56.535Z" },
{ url = "https://files.pythonhosted.org/packages/db/48/df670f75f458cc7c1752a01a394fd59c830b08172dd59cf29d73f31050f9/atheris-3.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a402cdca8a650d1371050b1f9552eb4cdc488d2db64950d603c4560318365eac", size = 34858525, upload-time = "2025-11-24T23:53:59.925Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "coverage"
version = "7.13.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" },
{ url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" },
{ url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" },
{ url = "https://files.pythonhosted.org/packages/76/53/c16972708cbb79f2942922571a687c52bd109a7bd51175aeb7558dff2236/coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", size = 252114, upload-time = "2026-02-09T12:56:35.749Z" },
{ url = "https://files.pythonhosted.org/packages/eb/c2/7ab36d8b8cc412bec9ea2d07c83c48930eb4ba649634ba00cb7e4e0f9017/coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", size = 254220, upload-time = "2026-02-09T12:56:37.796Z" },
{ url = "https://files.pythonhosted.org/packages/d6/4d/cf52c9a3322c89a0e6febdfbc83bb45c0ed3c64ad14081b9503adee702e7/coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", size = 256164, upload-time = "2026-02-09T12:56:39.016Z" },
{ url = "https://files.pythonhosted.org/packages/78/e9/eb1dd17bd6de8289df3580e967e78294f352a5df8a57ff4671ee5fc3dcd0/coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", size = 250325, upload-time = "2026-02-09T12:56:40.668Z" },
{ url = "https://files.pythonhosted.org/packages/71/07/8c1542aa873728f72267c07278c5cc0ec91356daf974df21335ccdb46368/coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", size = 251913, upload-time = "2026-02-09T12:56:41.97Z" },
{ url = "https://files.pythonhosted.org/packages/74/d7/c62e2c5e4483a748e27868e4c32ad3daa9bdddbba58e1bc7a15e252baa74/coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", size = 249974, upload-time = "2026-02-09T12:56:43.323Z" },
{ url = "https://files.pythonhosted.org/packages/98/9f/4c5c015a6e98ced54efd0f5cf8d31b88e5504ecb6857585fc0161bb1e600/coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", size = 253741, upload-time = "2026-02-09T12:56:45.155Z" },
{ url = "https://files.pythonhosted.org/packages/bd/59/0f4eef89b9f0fcd9633b5d350016f54126ab49426a70ff4c4e87446cabdc/coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", size = 249695, upload-time = "2026-02-09T12:56:46.636Z" },
{ url = "https://files.pythonhosted.org/packages/b5/2c/b7476f938deb07166f3eb281a385c262675d688ff4659ad56c6c6b8e2e70/coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", size = 250599, upload-time = "2026-02-09T12:56:48.13Z" },
{ url = "https://files.pythonhosted.org/packages/b8/34/c3420709d9846ee3785b9f2831b4d94f276f38884032dca1457fa83f7476/coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", size = 221780, upload-time = "2026-02-09T12:56:50.479Z" },
{ url = "https://files.pythonhosted.org/packages/61/08/3d9c8613079d2b11c185b865de9a4c1a68850cfda2b357fae365cf609f29/coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", size = 222715, upload-time = "2026-02-09T12:56:51.815Z" },
{ url = "https://files.pythonhosted.org/packages/18/1a/54c3c80b2f056164cc0a6cdcb040733760c7c4be9d780fe655f356f433e4/coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", size = 221385, upload-time = "2026-02-09T12:56:53.194Z" },
{ url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" },
{ url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" },
{ url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" },
{ url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" },
{ url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" },
{ url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" },
{ url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" },
{ url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" },
{ url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" },
{ url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" },
{ url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" },
{ url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" },
{ url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" },
{ url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" },
{ url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" },
{ url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" },
{ url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" },
{ url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" },
{ url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" },
{ url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" },
{ url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" },
{ url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" },
{ url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" },
{ url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" },
{ url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" },
{ url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" },
{ url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" },
{ url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" },
{ url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" },
{ url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" },
{ url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" },
{ url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" },
{ url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" },
{ url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" },
{ url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" },
{ url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" },
{ url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" },
{ url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" },
{ url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" },
{ url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" },
{ url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" },
{ url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" },
{ url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" },
{ url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" },
{ url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" },
{ url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" },
{ url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" },
{ url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" },
{ url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" },
{ url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" },
{ url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" },
{ url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" },
{ url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" },
{ url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" },
{ url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" },
{ url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" },
{ url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" },
{ url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" },
{ url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" },
{ url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" },
{ url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" },
{ url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" },
{ url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" },
{ url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" },
{ url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" },
{ url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" },
{ url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" },
{ url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" },
{ url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" },
{ url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" },
{ url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" },
{ url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" },
{ url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" },
{ url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" },
{ url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" },
{ url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" },
]
[package.optional-dependencies]
toml = [
{ name = "tomli", marker = "python_full_version <= '3.11'" },
]
[[package]]
name = "gitlab-skill"
version = "0.0.0"
source = { virtual = "." }
[package.dev-dependencies]
dev = [
{ name = "pytest" },
{ name = "pytest-cov" },
{ name = "pytest-mock" },
{ name = "ruff" },
]
fuzz = [
{ name = "atheris" },
]
[package.metadata]
[package.metadata.requires-dev]
dev = [
{ name = "pytest", specifier = ">=9.0" },
{ name = "pytest-cov", specifier = ">=7.0" },
{ name = "pytest-mock", specifier = ">=3.12" },
{ name = "ruff", specifier = ">=0.15" },
]
fuzz = [{ name = "atheris", specifier = ">=3.0" }]
[[package]]
name = "iniconfig"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]]
name = "packaging"
version = "26.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "pygments"
version = "2.20.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
]
[[package]]
name = "pytest"
version = "9.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
]
[[package]]
name = "pytest-cov"
version = "7.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "coverage", extra = ["toml"] },
{ name = "pluggy" },
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
]
[[package]]
name = "pytest-mock"
version = "3.15.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" },
]
[[package]]
name = "ruff"
version = "0.15.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/51/df/f8629c19c5318601d3121e230f74cbee7a3732339c52b21daa2b82ef9c7d/ruff-0.15.6.tar.gz", hash = "sha256:8394c7bb153a4e3811a4ecdacd4a8e6a4fa8097028119160dffecdcdf9b56ae4", size = 4597916, upload-time = "2026-03-12T23:05:47.51Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/2f/4e03a7e5ce99b517e98d3b4951f411de2b0fa8348d39cf446671adcce9a2/ruff-0.15.6-py3-none-linux_armv6l.whl", hash = "sha256:7c98c3b16407b2cf3d0f2b80c80187384bc92c6774d85fefa913ecd941256fff", size = 10508953, upload-time = "2026-03-12T23:05:17.246Z" },
{ url = "https://files.pythonhosted.org/packages/70/60/55bcdc3e9f80bcf39edf0cd272da6fa511a3d94d5a0dd9e0adf76ceebdb4/ruff-0.15.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee7dcfaad8b282a284df4aa6ddc2741b3f4a18b0555d626805555a820ea181c3", size = 10942257, upload-time = "2026-03-12T23:05:23.076Z" },
{ url = "https://files.pythonhosted.org/packages/e7/f9/005c29bd1726c0f492bfa215e95154cf480574140cb5f867c797c18c790b/ruff-0.15.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3bd9967851a25f038fc8b9ae88a7fbd1b609f30349231dffaa37b6804923c4bb", size = 10322683, upload-time = "2026-03-12T23:05:33.738Z" },
{ url = "https://files.pythonhosted.org/packages/5f/74/2f861f5fd7cbb2146bddb5501450300ce41562da36d21868c69b7a828169/ruff-0.15.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13f4594b04e42cd24a41da653886b04d2ff87adbf57497ed4f728b0e8a4866f8", size = 10660986, upload-time = "2026-03-12T23:05:53.245Z" },
{ url = "https://files.pythonhosted.org/packages/c1/a1/309f2364a424eccb763cdafc49df843c282609f47fe53aa83f38272389e0/ruff-0.15.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2ed8aea2f3fe57886d3f00ea5b8aae5bf68d5e195f487f037a955ff9fbaac9e", size = 10332177, upload-time = "2026-03-12T23:05:56.145Z" },
{ url = "https://files.pythonhosted.org/packages/30/41/7ebf1d32658b4bab20f8ac80972fb19cd4e2c6b78552be263a680edc55ac/ruff-0.15.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70789d3e7830b848b548aae96766431c0dc01a6c78c13381f423bf7076c66d15", size = 11170783, upload-time = "2026-03-12T23:06:01.742Z" },
{ url = "https://files.pythonhosted.org/packages/76/be/6d488f6adca047df82cd62c304638bcb00821c36bd4881cfca221561fdfc/ruff-0.15.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:542aaf1de3154cea088ced5a819ce872611256ffe2498e750bbae5247a8114e9", size = 12044201, upload-time = "2026-03-12T23:05:28.697Z" },
{ url = "https://files.pythonhosted.org/packages/71/68/e6f125df4af7e6d0b498f8d373274794bc5156b324e8ab4bf5c1b4fc0ec7/ruff-0.15.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c22e6f02c16cfac3888aa636e9eba857254d15bbacc9906c9689fdecb1953ab", size = 11421561, upload-time = "2026-03-12T23:05:31.236Z" },
{ url = "https://files.pythonhosted.org/packages/f1/9f/f85ef5fd01a52e0b472b26dc1b4bd228b8f6f0435975442ffa4741278703/ruff-0.15.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98893c4c0aadc8e448cfa315bd0cc343a5323d740fe5f28ef8a3f9e21b381f7e", size = 11310928, upload-time = "2026-03-12T23:05:45.288Z" },
{ url = "https://files.pythonhosted.org/packages/8c/26/b75f8c421f5654304b89471ed384ae8c7f42b4dff58fa6ce1626d7f2b59a/ruff-0.15.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:70d263770d234912374493e8cc1e7385c5d49376e41dfa51c5c3453169dc581c", size = 11235186, upload-time = "2026-03-12T23:05:50.677Z" },
{ url = "https://files.pythonhosted.org/packages/fc/d4/d5a6d065962ff7a68a86c9b4f5500f7d101a0792078de636526c0edd40da/ruff-0.15.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:55a1ad63c5a6e54b1f21b7514dfadc0c7fb40093fa22e95143cf3f64ebdcd512", size = 10635231, upload-time = "2026-03-12T23:05:37.044Z" },
{ url = "https://files.pythonhosted.org/packages/d6/56/7c3acf3d50910375349016cf33de24be021532042afbed87942858992491/ruff-0.15.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8dc473ba093c5ec238bb1e7429ee676dca24643c471e11fbaa8a857925b061c0", size = 10340357, upload-time = "2026-03-12T23:06:04.748Z" },
{ url = "https://files.pythonhosted.org/packages/06/54/6faa39e9c1033ff6a3b6e76b5df536931cd30caf64988e112bbf91ef5ce5/ruff-0.15.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:85b042377c2a5561131767974617006f99f7e13c63c111b998f29fc1e58a4cfb", size = 10860583, upload-time = "2026-03-12T23:05:58.978Z" },
{ url = "https://files.pythonhosted.org/packages/cb/1e/509a201b843b4dfb0b32acdedf68d951d3377988cae43949ba4c4133a96a/ruff-0.15.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cef49e30bc5a86a6a92098a7fbf6e467a234d90b63305d6f3ec01225a9d092e0", size = 11410976, upload-time = "2026-03-12T23:05:39.955Z" },
{ url = "https://files.pythonhosted.org/packages/6c/25/3fc9114abf979a41673ce877c08016f8e660ad6cf508c3957f537d2e9fa9/ruff-0.15.6-py3-none-win32.whl", hash = "sha256:bbf67d39832404812a2d23020dda68fee7f18ce15654e96fb1d3ad21a5fe436c", size = 10616872, upload-time = "2026-03-12T23:05:42.451Z" },
{ url = "https://files.pythonhosted.org/packages/89/7a/09ece68445ceac348df06e08bf75db72d0e8427765b96c9c0ffabc1be1d9/ruff-0.15.6-py3-none-win_amd64.whl", hash = "sha256:aee25bc84c2f1007ecb5037dff75cef00414fdf17c23f07dc13e577883dca406", size = 11787271, upload-time = "2026-03-12T23:05:20.168Z" },
{ url = "https://files.pythonhosted.org/packages/7f/d0/578c47dd68152ddddddf31cd7fc67dc30b7cdf639a86275fda821b0d9d98/ruff-0.15.6-py3-none-win_arm64.whl", hash = "sha256:c34de3dd0b0ba203be50ae70f5910b17188556630e2178fd7d79fc030eb0d837", size = 11060497, upload-time = "2026-03-12T23:05:25.968Z" },
]
[[package]]
name = "tomli"
version = "2.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" },
{ url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" },
{ url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" },
{ url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" },
{ url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" },
{ url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" },
{ url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" },
{ url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" },
{ url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" },
{ url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" },
{ url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" },
{ url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" },
{ url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" },
{ url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" },
{ url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" },
{ url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" },
{ url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" },
{ url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" },
{ url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" },
{ url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" },
{ url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" },
{ url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" },
{ url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" },
{ url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" },
{ url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" },
{ url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" },
{ url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" },
{ url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" },
{ url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" },
{ url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" },
{ url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" },
{ url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" },
{ url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" },
{ url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" },
{ url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" },
{ url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" },
{ url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" },
{ url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" },
{ url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" },
{ url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" },
{ url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" },
{ url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" },
{ url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" },
{ url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" },
{ url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" },
{ url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
]
Related skills
FAQ
What does gitlab do?
gitlab provides GitLab security workflow guidance from hve-core.
When should I use gitlab?
GitLab security or workflow questions in hve-core context.
Is this skill safe to install?
Review the Security Audits panel on this page before installing in production.