This commit is contained in:
2024-12-09 18:22:38 +09:00
parent ab0cbebefc
commit c4c4547706
959 changed files with 174888 additions and 6 deletions

View File

View File

@ -0,0 +1,605 @@
import argparse
import json
import operator
import sys
from typing import Any, Dict, List, Optional, Type, TYPE_CHECKING, Union
import gitlab
import gitlab.base
import gitlab.v4.objects
from gitlab import cli
from gitlab.exceptions import GitlabCiLintError
class GitlabCLI:
def __init__(
self,
gl: gitlab.Gitlab,
gitlab_resource: str,
resource_action: str,
args: Dict[str, str],
) -> None:
self.cls: Type[gitlab.base.RESTObject] = cli.gitlab_resource_to_cls(
gitlab_resource, namespace=gitlab.v4.objects
)
self.cls_name = self.cls.__name__
self.gitlab_resource = gitlab_resource.replace("-", "_")
self.resource_action = resource_action.lower()
self.gl = gl
self.args = args
self.parent_args: Dict[str, Any] = {}
self.mgr_cls: Union[
Type[gitlab.mixins.CreateMixin],
Type[gitlab.mixins.DeleteMixin],
Type[gitlab.mixins.GetMixin],
Type[gitlab.mixins.GetWithoutIdMixin],
Type[gitlab.mixins.ListMixin],
Type[gitlab.mixins.UpdateMixin],
] = getattr(gitlab.v4.objects, f"{self.cls.__name__}Manager")
# We could do something smart, like splitting the manager name to find
# parents, build the chain of managers to get to the final object.
# Instead we do something ugly and efficient: interpolate variables in
# the class _path attribute, and replace the value with the result.
if TYPE_CHECKING:
assert self.mgr_cls._path is not None
self._process_from_parent_attrs()
self.mgr_cls._path = self.mgr_cls._path.format(**self.parent_args)
self.mgr = self.mgr_cls(gl)
self.mgr._from_parent_attrs = self.parent_args
if self.mgr_cls._types:
for attr_name, type_cls in self.mgr_cls._types.items():
if attr_name in self.args.keys():
obj = type_cls()
obj.set_from_cli(self.args[attr_name])
self.args[attr_name] = obj.get()
def _process_from_parent_attrs(self) -> None:
"""Items in the path need to be url-encoded. There is a 1:1 mapping from
mgr_cls._from_parent_attrs <--> mgr_cls._path. Those values must be url-encoded
as they may contain a slash '/'."""
for key in self.mgr_cls._from_parent_attrs:
if key not in self.args:
continue
self.parent_args[key] = gitlab.utils.EncodedId(self.args[key])
# If we don't delete it then it will be added to the URL as a query-string
del self.args[key]
def run(self) -> Any:
# Check for a method that matches gitlab_resource + action
method = f"do_{self.gitlab_resource}_{self.resource_action}"
if hasattr(self, method):
return getattr(self, method)()
# Fallback to standard actions (get, list, create, ...)
method = f"do_{self.resource_action}"
if hasattr(self, method):
return getattr(self, method)()
# Finally try to find custom methods
return self.do_custom()
def do_custom(self) -> Any:
class_instance: Union[gitlab.base.RESTManager, gitlab.base.RESTObject]
in_obj = cli.custom_actions[self.cls_name][self.resource_action].in_object
# Get the object (lazy), then act
if in_obj:
data = {}
if self.mgr._from_parent_attrs:
for k in self.mgr._from_parent_attrs:
data[k] = self.parent_args[k]
if not issubclass(self.cls, gitlab.mixins.GetWithoutIdMixin):
if TYPE_CHECKING:
assert isinstance(self.cls._id_attr, str)
data[self.cls._id_attr] = self.args.pop(self.cls._id_attr)
class_instance = self.cls(self.mgr, data)
else:
class_instance = self.mgr
method_name = self.resource_action.replace("-", "_")
return getattr(class_instance, method_name)(**self.args)
def do_project_export_download(self) -> None:
try:
project = self.gl.projects.get(self.parent_args["project_id"], lazy=True)
export_status = project.exports.get()
if TYPE_CHECKING:
assert export_status is not None
data = export_status.download()
if TYPE_CHECKING:
assert data is not None
assert isinstance(data, bytes)
sys.stdout.buffer.write(data)
except Exception as e: # pragma: no cover, cli.die is unit-tested
cli.die("Impossible to download the export", e)
def do_validate(self) -> None:
if TYPE_CHECKING:
assert isinstance(self.mgr, gitlab.v4.objects.CiLintManager)
try:
self.mgr.validate(self.args)
except GitlabCiLintError as e: # pragma: no cover, cli.die is unit-tested
cli.die("CI YAML Lint failed", e)
except Exception as e: # pragma: no cover, cli.die is unit-tested
cli.die("Cannot validate CI YAML", e)
def do_create(self) -> gitlab.base.RESTObject:
if TYPE_CHECKING:
assert isinstance(self.mgr, gitlab.mixins.CreateMixin)
try:
result = self.mgr.create(self.args)
except Exception as e: # pragma: no cover, cli.die is unit-tested
cli.die("Impossible to create object", e)
return result
def do_list(
self,
) -> Union[gitlab.base.RESTObjectList, List[gitlab.base.RESTObject]]:
if TYPE_CHECKING:
assert isinstance(self.mgr, gitlab.mixins.ListMixin)
message_details = gitlab.utils.WarnMessageData(
message=(
"Your query returned {len_items} of {total_items} items. To return all "
"items use `--get-all`. To silence this warning use `--no-get-all`."
),
show_caller=False,
)
try:
result = self.mgr.list(**self.args, message_details=message_details)
except Exception as e: # pragma: no cover, cli.die is unit-tested
cli.die("Impossible to list objects", e)
return result
def do_get(self) -> Optional[gitlab.base.RESTObject]:
if isinstance(self.mgr, gitlab.mixins.GetWithoutIdMixin):
try:
result = self.mgr.get(id=None, **self.args)
except Exception as e: # pragma: no cover, cli.die is unit-tested
cli.die("Impossible to get object", e)
return result
if TYPE_CHECKING:
assert isinstance(self.mgr, gitlab.mixins.GetMixin)
assert isinstance(self.cls._id_attr, str)
id = self.args.pop(self.cls._id_attr)
try:
result = self.mgr.get(id, lazy=False, **self.args)
except Exception as e: # pragma: no cover, cli.die is unit-tested
cli.die("Impossible to get object", e)
return result
def do_delete(self) -> None:
if TYPE_CHECKING:
assert isinstance(self.mgr, gitlab.mixins.DeleteMixin)
assert isinstance(self.cls._id_attr, str)
id = self.args.pop(self.cls._id_attr)
try:
self.mgr.delete(id, **self.args)
except Exception as e: # pragma: no cover, cli.die is unit-tested
cli.die("Impossible to destroy object", e)
def do_update(self) -> Dict[str, Any]:
if TYPE_CHECKING:
assert isinstance(self.mgr, gitlab.mixins.UpdateMixin)
if issubclass(self.mgr_cls, gitlab.mixins.GetWithoutIdMixin):
id = None
else:
if TYPE_CHECKING:
assert isinstance(self.cls._id_attr, str)
id = self.args.pop(self.cls._id_attr)
try:
result = self.mgr.update(id, self.args)
except Exception as e: # pragma: no cover, cli.die is unit-tested
cli.die("Impossible to update object", e)
return result
# https://github.com/python/typeshed/issues/7539#issuecomment-1076581049
if TYPE_CHECKING:
_SubparserType = argparse._SubParsersAction[argparse.ArgumentParser]
else:
_SubparserType = Any
def _populate_sub_parser_by_class(
cls: Type[gitlab.base.RESTObject],
sub_parser: _SubparserType,
) -> None:
mgr_cls_name = f"{cls.__name__}Manager"
mgr_cls = getattr(gitlab.v4.objects, mgr_cls_name)
action_parsers: Dict[str, argparse.ArgumentParser] = {}
for action_name, help_text in [
("list", "List the GitLab resources"),
("get", "Get a GitLab resource"),
("create", "Create a GitLab resource"),
("update", "Update a GitLab resource"),
("delete", "Delete a GitLab resource"),
]:
if not hasattr(mgr_cls, action_name):
continue
sub_parser_action = sub_parser.add_parser(
action_name,
conflict_handler="resolve",
help=help_text,
)
action_parsers[action_name] = sub_parser_action
sub_parser_action.add_argument("--sudo", required=False)
if mgr_cls._from_parent_attrs:
for x in mgr_cls._from_parent_attrs:
sub_parser_action.add_argument(
f"--{x.replace('_', '-')}", required=True
)
if action_name == "list":
for x in mgr_cls._list_filters:
sub_parser_action.add_argument(
f"--{x.replace('_', '-')}", required=False
)
sub_parser_action.add_argument("--page", required=False, type=int)
sub_parser_action.add_argument("--per-page", required=False, type=int)
get_all_group = sub_parser_action.add_mutually_exclusive_group()
get_all_group.add_argument(
"--get-all",
required=False,
action="store_const",
const=True,
default=None,
dest="get_all",
help="Return all items from the server, without pagination.",
)
get_all_group.add_argument(
"--no-get-all",
required=False,
action="store_const",
const=False,
default=None,
dest="get_all",
help="Don't return all items from the server.",
)
if action_name == "delete":
if cls._id_attr is not None:
id_attr = cls._id_attr.replace("_", "-")
sub_parser_action.add_argument(f"--{id_attr}", required=True)
if action_name == "get":
if not issubclass(cls, gitlab.mixins.GetWithoutIdMixin):
if cls._id_attr is not None:
id_attr = cls._id_attr.replace("_", "-")
sub_parser_action.add_argument(f"--{id_attr}", required=True)
for x in mgr_cls._optional_get_attrs:
sub_parser_action.add_argument(
f"--{x.replace('_', '-')}", required=False
)
if action_name == "create":
for x in mgr_cls._create_attrs.required:
sub_parser_action.add_argument(
f"--{x.replace('_', '-')}", required=True
)
for x in mgr_cls._create_attrs.optional:
sub_parser_action.add_argument(
f"--{x.replace('_', '-')}", required=False
)
if mgr_cls._create_attrs.exclusive:
group = sub_parser_action.add_mutually_exclusive_group()
for x in mgr_cls._create_attrs.exclusive:
group.add_argument(f"--{x.replace('_', '-')}")
if action_name == "update":
if cls._id_attr is not None:
id_attr = cls._id_attr.replace("_", "-")
sub_parser_action.add_argument(f"--{id_attr}", required=True)
for x in mgr_cls._update_attrs.required:
if x != cls._id_attr:
sub_parser_action.add_argument(
f"--{x.replace('_', '-')}", required=True
)
for x in mgr_cls._update_attrs.optional:
if x != cls._id_attr:
sub_parser_action.add_argument(
f"--{x.replace('_', '-')}", required=False
)
if mgr_cls._update_attrs.exclusive:
group = sub_parser_action.add_mutually_exclusive_group()
for x in mgr_cls._update_attrs.exclusive:
group.add_argument(f"--{x.replace('_', '-')}")
if cls.__name__ in cli.custom_actions:
name = cls.__name__
for action_name in cli.custom_actions[name]:
custom_action = cli.custom_actions[name][action_name]
# NOTE(jlvillal): If we put a function for the `default` value of
# the `get` it will always get called, which will break things.
action_parser = action_parsers.get(action_name)
if action_parser is None:
sub_parser_action = sub_parser.add_parser(
action_name, help=custom_action.help
)
else:
sub_parser_action = action_parser
# Get the attributes for URL/path construction
if mgr_cls._from_parent_attrs:
for x in mgr_cls._from_parent_attrs:
sub_parser_action.add_argument(
f"--{x.replace('_', '-')}", required=True
)
sub_parser_action.add_argument("--sudo", required=False)
# We need to get the object somehow
if not issubclass(cls, gitlab.mixins.GetWithoutIdMixin):
if cls._id_attr is not None and custom_action.requires_id:
id_attr = cls._id_attr.replace("_", "-")
sub_parser_action.add_argument(f"--{id_attr}", required=True)
for x in custom_action.required:
if x != cls._id_attr:
sub_parser_action.add_argument(
f"--{x.replace('_', '-')}", required=True
)
for x in custom_action.optional:
if x != cls._id_attr:
sub_parser_action.add_argument(
f"--{x.replace('_', '-')}", required=False
)
if mgr_cls.__name__ in cli.custom_actions:
name = mgr_cls.__name__
for action_name in cli.custom_actions[name]:
# NOTE(jlvillal): If we put a function for the `default` value of
# the `get` it will always get called, which will break things.
action_parser = action_parsers.get(action_name)
if action_parser is None:
sub_parser_action = sub_parser.add_parser(action_name)
else:
sub_parser_action = action_parser
if mgr_cls._from_parent_attrs:
for x in mgr_cls._from_parent_attrs:
sub_parser_action.add_argument(
f"--{x.replace('_', '-')}", required=True
)
sub_parser_action.add_argument("--sudo", required=False)
custom_action = cli.custom_actions[name][action_name]
for x in custom_action.required:
if x != cls._id_attr:
sub_parser_action.add_argument(
f"--{x.replace('_', '-')}", required=True
)
for x in custom_action.optional:
if x != cls._id_attr:
sub_parser_action.add_argument(
f"--{x.replace('_', '-')}", required=False
)
def extend_parser(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
subparsers = parser.add_subparsers(
title="resource",
dest="gitlab_resource",
help="The GitLab resource to manipulate.",
)
subparsers.required = True
# populate argparse for all Gitlab Object
classes = set()
for cls in gitlab.v4.objects.__dict__.values():
if not isinstance(cls, type):
continue
if issubclass(cls, gitlab.base.RESTManager):
if cls._obj_cls is not None:
classes.add(cls._obj_cls)
for cls in sorted(classes, key=operator.attrgetter("__name__")):
arg_name = cli.cls_to_gitlab_resource(cls)
mgr_cls_name = f"{cls.__name__}Manager"
mgr_cls = getattr(gitlab.v4.objects, mgr_cls_name)
object_group = subparsers.add_parser(
arg_name,
help=f"API endpoint: {mgr_cls._path}",
)
object_subparsers = object_group.add_subparsers(
title="action",
dest="resource_action",
help="Action to execute on the GitLab resource.",
)
_populate_sub_parser_by_class(cls, object_subparsers)
object_subparsers.required = True
return parser
def get_dict(
obj: Union[str, Dict[str, Any], gitlab.base.RESTObject], fields: List[str]
) -> Union[str, Dict[str, Any]]:
if not isinstance(obj, gitlab.base.RESTObject):
return obj
if fields:
return {k: v for k, v in obj.attributes.items() if k in fields}
return obj.attributes
class JSONPrinter:
@staticmethod
def display(d: Union[str, Dict[str, Any]], **_kwargs: Any) -> None:
print(json.dumps(d))
@staticmethod
def display_list(
data: List[Union[str, Dict[str, Any], gitlab.base.RESTObject]],
fields: List[str],
**_kwargs: Any,
) -> None:
print(json.dumps([get_dict(obj, fields) for obj in data]))
class YAMLPrinter:
@staticmethod
def display(d: Union[str, Dict[str, Any]], **_kwargs: Any) -> None:
try:
import yaml # noqa
print(yaml.safe_dump(d, default_flow_style=False))
except ImportError:
sys.exit(
"PyYaml is not installed.\n"
"Install it with `pip install PyYaml` "
"to use the yaml output feature"
)
@staticmethod
def display_list(
data: List[Union[str, Dict[str, Any], gitlab.base.RESTObject]],
fields: List[str],
**_kwargs: Any,
) -> None:
try:
import yaml # noqa
print(
yaml.safe_dump(
[get_dict(obj, fields) for obj in data], default_flow_style=False
)
)
except ImportError:
sys.exit(
"PyYaml is not installed.\n"
"Install it with `pip install PyYaml` "
"to use the yaml output feature"
)
class LegacyPrinter:
def display(self, _d: Union[str, Dict[str, Any]], **kwargs: Any) -> None:
verbose = kwargs.get("verbose", False)
padding = kwargs.get("padding", 0)
obj: Optional[Union[Dict[str, Any], gitlab.base.RESTObject]] = kwargs.get("obj")
if TYPE_CHECKING:
assert obj is not None
def display_dict(d: Dict[str, Any], padding: int) -> None:
for k in sorted(d.keys()):
v = d[k]
if isinstance(v, dict):
print(f"{' ' * padding}{k.replace('_', '-')}:")
new_padding = padding + 2
self.display(v, verbose=True, padding=new_padding, obj=v)
continue
print(f"{' ' * padding}{k.replace('_', '-')}: {v}")
if verbose:
if isinstance(obj, dict):
display_dict(obj, padding)
return
# not a dict, we assume it's a RESTObject
if obj._id_attr:
id = getattr(obj, obj._id_attr, None)
print(f"{obj._id_attr}: {id}")
attrs = obj.attributes
if obj._id_attr:
attrs.pop(obj._id_attr)
display_dict(attrs, padding)
return
lines = []
if TYPE_CHECKING:
assert isinstance(obj, gitlab.base.RESTObject)
if obj._id_attr:
id = getattr(obj, obj._id_attr)
lines.append(f"{obj._id_attr.replace('_', '-')}: {id}")
if obj._repr_attr:
value = getattr(obj, obj._repr_attr, "None") or "None"
value = value.replace("\r", "").replace("\n", " ")
# If the attribute is a note (ProjectCommitComment) then we do
# some modifications to fit everything on one line
line = f"{obj._repr_attr}: {value}"
# ellipsize long lines (comments)
if len(line) > 79:
line = f"{line[:76]}..."
lines.append(line)
if lines:
print("\n".join(lines))
return
print(
f"No default fields to show for {obj!r}. "
f"Please use '--verbose' or the JSON/YAML formatters."
)
def display_list(
self,
data: List[Union[str, gitlab.base.RESTObject]],
fields: List[str],
**kwargs: Any,
) -> None:
verbose = kwargs.get("verbose", False)
for obj in data:
if isinstance(obj, gitlab.base.RESTObject):
self.display(get_dict(obj, fields), verbose=verbose, obj=obj)
else:
print(obj)
print("")
PRINTERS: Dict[
str, Union[Type[JSONPrinter], Type[LegacyPrinter], Type[YAMLPrinter]]
] = {
"json": JSONPrinter,
"legacy": LegacyPrinter,
"yaml": YAMLPrinter,
}
def run(
gl: gitlab.Gitlab,
gitlab_resource: str,
resource_action: str,
args: Dict[str, Any],
verbose: bool,
output: str,
fields: List[str],
) -> None:
g_cli = GitlabCLI(
gl=gl,
gitlab_resource=gitlab_resource,
resource_action=resource_action,
args=args,
)
data = g_cli.run()
printer: Union[JSONPrinter, LegacyPrinter, YAMLPrinter] = PRINTERS[output]()
if isinstance(data, dict):
printer.display(data, verbose=True, obj=data)
elif isinstance(data, list):
printer.display_list(data, fields, verbose=verbose)
elif isinstance(data, gitlab.base.RESTObjectList):
printer.display_list(list(data), fields, verbose=verbose)
elif isinstance(data, gitlab.base.RESTObject):
printer.display(get_dict(data, fields), verbose=verbose, obj=data)
elif isinstance(data, str):
print(data)
elif isinstance(data, bytes):
sys.stdout.buffer.write(data)
elif hasattr(data, "decode"):
print(data.decode())

View File

@ -0,0 +1,79 @@
from .access_requests import *
from .appearance import *
from .applications import *
from .artifacts import *
from .audit_events import *
from .award_emojis import *
from .badges import *
from .boards import *
from .branches import *
from .broadcast_messages import *
from .bulk_imports import *
from .ci_lint import *
from .cluster_agents import *
from .clusters import *
from .commits import *
from .container_registry import *
from .custom_attributes import *
from .deploy_keys import *
from .deploy_tokens import *
from .deployments import *
from .discussions import *
from .draft_notes import *
from .environments import *
from .epics import *
from .events import *
from .export_import import *
from .features import *
from .files import *
from .geo_nodes import *
from .group_access_tokens import *
from .groups import *
from .hooks import *
from .integrations import *
from .invitations import *
from .issues import *
from .iterations import *
from .job_token_scope import *
from .jobs import *
from .keys import *
from .labels import *
from .ldap import *
from .members import *
from .merge_request_approvals import *
from .merge_requests import *
from .merge_trains import *
from .milestones import *
from .namespaces import *
from .notes import *
from .notification_settings import *
from .package_protection_rules import *
from .packages import *
from .pages import *
from .personal_access_tokens import *
from .pipelines import *
from .project_access_tokens import *
from .projects import *
from .push_rules import *
from .registry_protection_rules import *
from .releases import *
from .repositories import *
from .resource_groups import *
from .reviewers import *
from .runners import *
from .secure_files import *
from .service_accounts import *
from .settings import *
from .sidekiq import *
from .snippets import *
from .statistics import *
from .tags import *
from .templates import *
from .todos import *
from .topics import *
from .triggers import *
from .users import *
from .variables import *
from .wikis import *
__all__ = [name for name in dir() if not name.startswith("_")]

View File

@ -0,0 +1,35 @@
from gitlab.base import RESTManager, RESTObject
from gitlab.mixins import (
AccessRequestMixin,
CreateMixin,
DeleteMixin,
ListMixin,
ObjectDeleteMixin,
)
__all__ = [
"GroupAccessRequest",
"GroupAccessRequestManager",
"ProjectAccessRequest",
"ProjectAccessRequestManager",
]
class GroupAccessRequest(AccessRequestMixin, ObjectDeleteMixin, RESTObject):
pass
class GroupAccessRequestManager(ListMixin, CreateMixin, DeleteMixin, RESTManager):
_path = "/groups/{group_id}/access_requests"
_obj_cls = GroupAccessRequest
_from_parent_attrs = {"group_id": "id"}
class ProjectAccessRequest(AccessRequestMixin, ObjectDeleteMixin, RESTObject):
pass
class ProjectAccessRequestManager(ListMixin, CreateMixin, DeleteMixin, RESTManager):
_path = "/projects/{project_id}/access_requests"
_obj_cls = ProjectAccessRequest
_from_parent_attrs = {"project_id": "id"}

View File

@ -0,0 +1,63 @@
from typing import Any, cast, Dict, Optional, Union
from gitlab import exceptions as exc
from gitlab.base import RESTManager, RESTObject
from gitlab.mixins import GetWithoutIdMixin, SaveMixin, UpdateMixin
from gitlab.types import RequiredOptional
__all__ = [
"ApplicationAppearance",
"ApplicationAppearanceManager",
]
class ApplicationAppearance(SaveMixin, RESTObject):
_id_attr = None
class ApplicationAppearanceManager(GetWithoutIdMixin, UpdateMixin, RESTManager):
_path = "/application/appearance"
_obj_cls = ApplicationAppearance
_update_attrs = RequiredOptional(
optional=(
"title",
"description",
"logo",
"header_logo",
"favicon",
"new_project_guidelines",
"header_message",
"footer_message",
"message_background_color",
"message_font_color",
"email_header_and_footer_enabled",
),
)
@exc.on_http_error(exc.GitlabUpdateError)
def update(
self,
id: Optional[Union[str, int]] = None,
new_data: Optional[Dict[str, Any]] = None,
**kwargs: Any,
) -> Dict[str, Any]:
"""Update an object on the server.
Args:
id: ID of the object to update (can be None if not required)
new_data: the update data for the object
**kwargs: Extra options to send to the server (e.g. sudo)
Returns:
The new object data (*not* a RESTObject)
Raises:
GitlabAuthenticationError: If authentication is not correct
GitlabUpdateError: If the server cannot perform the request
"""
new_data = new_data or {}
data = new_data.copy()
return super().update(id, data, **kwargs)
def get(self, **kwargs: Any) -> ApplicationAppearance:
return cast(ApplicationAppearance, super().get(**kwargs))

View File

@ -0,0 +1,21 @@
from gitlab.base import RESTManager, RESTObject
from gitlab.mixins import CreateMixin, DeleteMixin, ListMixin, ObjectDeleteMixin
from gitlab.types import RequiredOptional
__all__ = [
"Application",
"ApplicationManager",
]
class Application(ObjectDeleteMixin, RESTObject):
_url = "/applications"
_repr_attr = "name"
class ApplicationManager(ListMixin, CreateMixin, DeleteMixin, RESTManager):
_path = "/applications"
_obj_cls = Application
_create_attrs = RequiredOptional(
required=("name", "redirect_uri", "scopes"), optional=("confidential",)
)

View File

@ -0,0 +1,147 @@
"""
GitLab API:
https://docs.gitlab.com/ee/api/job_artifacts.html
"""
from typing import Any, Callable, Iterator, Optional, TYPE_CHECKING, Union
import requests
from gitlab import cli
from gitlab import exceptions as exc
from gitlab import utils
from gitlab.base import RESTManager, RESTObject
__all__ = ["ProjectArtifact", "ProjectArtifactManager"]
class ProjectArtifact(RESTObject):
"""Dummy object to manage custom actions on artifacts"""
_id_attr = "ref_name"
class ProjectArtifactManager(RESTManager):
_obj_cls = ProjectArtifact
_path = "/projects/{project_id}/jobs/artifacts"
_from_parent_attrs = {"project_id": "id"}
@exc.on_http_error(exc.GitlabDeleteError)
def delete(self, **kwargs: Any) -> None:
"""Delete the project's artifacts on the server.
Args:
**kwargs: Extra options to send to the server (e.g. sudo)
Raises:
GitlabAuthenticationError: If authentication is not correct
GitlabDeleteError: If the server cannot perform the request
"""
path = self._compute_path("/projects/{project_id}/artifacts")
if TYPE_CHECKING:
assert path is not None
self.gitlab.http_delete(path, **kwargs)
@cli.register_custom_action(
cls_names="ProjectArtifactManager",
required=("ref_name", "job"),
optional=("job_token",),
)
@exc.on_http_error(exc.GitlabGetError)
def download(
self,
ref_name: str,
job: str,
streamed: bool = False,
action: Optional[Callable[[bytes], None]] = None,
chunk_size: int = 1024,
*,
iterator: bool = False,
**kwargs: Any,
) -> Optional[Union[bytes, Iterator[Any]]]:
"""Get the job artifacts archive from a specific tag or branch.
Args:
ref_name: Branch or tag name in repository. HEAD or SHA references
are not supported.
job: The name of the job.
job_token: Job token for multi-project pipeline triggers.
streamed: If True the data will be processed by chunks of
`chunk_size` and each chunk is passed to `action` for
treatment
iterator: If True directly return the underlying response
iterator
action: Callable responsible of dealing with chunk of
data
chunk_size: Size of each chunk
**kwargs: Extra options to send to the server (e.g. sudo)
Raises:
GitlabAuthenticationError: If authentication is not correct
GitlabGetError: If the artifacts could not be retrieved
Returns:
The artifacts if `streamed` is False, None otherwise.
"""
path = f"{self.path}/{ref_name}/download"
result = self.gitlab.http_get(
path, job=job, streamed=streamed, raw=True, **kwargs
)
if TYPE_CHECKING:
assert isinstance(result, requests.Response)
return utils.response_content(
result, streamed, action, chunk_size, iterator=iterator
)
@cli.register_custom_action(
cls_names="ProjectArtifactManager",
required=("ref_name", "artifact_path", "job"),
)
@exc.on_http_error(exc.GitlabGetError)
def raw(
self,
ref_name: str,
artifact_path: str,
job: str,
streamed: bool = False,
action: Optional[Callable[[bytes], None]] = None,
chunk_size: int = 1024,
*,
iterator: bool = False,
**kwargs: Any,
) -> Optional[Union[bytes, Iterator[Any]]]:
"""Download a single artifact file from a specific tag or branch from
within the job's artifacts archive.
Args:
ref_name: Branch or tag name in repository. HEAD or SHA references
are not supported.
artifact_path: Path to a file inside the artifacts archive.
job: The name of the job.
streamed: If True the data will be processed by chunks of
`chunk_size` and each chunk is passed to `action` for
treatment
iterator: If True directly return the underlying response
iterator
action: Callable responsible of dealing with chunk of
data
chunk_size: Size of each chunk
**kwargs: Extra options to send to the server (e.g. sudo)
Raises:
GitlabAuthenticationError: If authentication is not correct
GitlabGetError: If the artifacts could not be retrieved
Returns:
The artifact if `streamed` is False, None otherwise.
"""
path = f"{self.path}/{ref_name}/raw/{artifact_path}"
result = self.gitlab.http_get(
path, streamed=streamed, raw=True, job=job, **kwargs
)
if TYPE_CHECKING:
assert isinstance(result, requests.Response)
return utils.response_content(
result, streamed, action, chunk_size, iterator=iterator
)

View File

@ -0,0 +1,73 @@
"""
GitLab API:
https://docs.gitlab.com/ee/api/audit_events.html
"""
from typing import Any, cast, Union
from gitlab.base import RESTManager, RESTObject
from gitlab.mixins import RetrieveMixin
__all__ = [
"AuditEvent",
"AuditEventManager",
"GroupAuditEvent",
"GroupAuditEventManager",
"ProjectAuditEvent",
"ProjectAuditEventManager",
"ProjectAudit",
"ProjectAuditManager",
]
class AuditEvent(RESTObject):
_id_attr = "id"
class AuditEventManager(RetrieveMixin, RESTManager):
_path = "/audit_events"
_obj_cls = AuditEvent
_list_filters = ("created_after", "created_before", "entity_type", "entity_id")
def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> AuditEvent:
return cast(AuditEvent, super().get(id=id, lazy=lazy, **kwargs))
class GroupAuditEvent(RESTObject):
_id_attr = "id"
class GroupAuditEventManager(RetrieveMixin, RESTManager):
_path = "/groups/{group_id}/audit_events"
_obj_cls = GroupAuditEvent
_from_parent_attrs = {"group_id": "id"}
_list_filters = ("created_after", "created_before")
def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
) -> GroupAuditEvent:
return cast(GroupAuditEvent, super().get(id=id, lazy=lazy, **kwargs))
class ProjectAuditEvent(RESTObject):
_id_attr = "id"
class ProjectAuditEventManager(RetrieveMixin, RESTManager):
_path = "/projects/{project_id}/audit_events"
_obj_cls = ProjectAuditEvent
_from_parent_attrs = {"project_id": "id"}
_list_filters = ("created_after", "created_before")
def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
) -> ProjectAuditEvent:
return cast(ProjectAuditEvent, super().get(id=id, lazy=lazy, **kwargs))
class ProjectAudit(ProjectAuditEvent):
pass
class ProjectAuditManager(ProjectAuditEventManager):
pass

View File

@ -0,0 +1,174 @@
from typing import Any, cast, Union
from gitlab.base import RESTManager, RESTObject
from gitlab.mixins import NoUpdateMixin, ObjectDeleteMixin
from gitlab.types import RequiredOptional
__all__ = [
"GroupEpicAwardEmoji",
"GroupEpicAwardEmojiManager",
"GroupEpicNoteAwardEmoji",
"GroupEpicNoteAwardEmojiManager",
"ProjectIssueAwardEmoji",
"ProjectIssueAwardEmojiManager",
"ProjectIssueNoteAwardEmoji",
"ProjectIssueNoteAwardEmojiManager",
"ProjectMergeRequestAwardEmoji",
"ProjectMergeRequestAwardEmojiManager",
"ProjectMergeRequestNoteAwardEmoji",
"ProjectMergeRequestNoteAwardEmojiManager",
"ProjectSnippetAwardEmoji",
"ProjectSnippetAwardEmojiManager",
"ProjectSnippetNoteAwardEmoji",
"ProjectSnippetNoteAwardEmojiManager",
]
class GroupEpicAwardEmoji(ObjectDeleteMixin, RESTObject):
pass
class GroupEpicAwardEmojiManager(NoUpdateMixin, RESTManager):
_path = "/groups/{group_id}/epics/{epic_iid}/award_emoji"
_obj_cls = GroupEpicAwardEmoji
_from_parent_attrs = {"group_id": "group_id", "epic_iid": "iid"}
_create_attrs = RequiredOptional(required=("name",))
def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
) -> GroupEpicAwardEmoji:
return cast(GroupEpicAwardEmoji, super().get(id=id, lazy=lazy, **kwargs))
class GroupEpicNoteAwardEmoji(ObjectDeleteMixin, RESTObject):
pass
class GroupEpicNoteAwardEmojiManager(NoUpdateMixin, RESTManager):
_path = "/groups/{group_id}/epics/{epic_iid}/notes/{note_id}/award_emoji"
_obj_cls = GroupEpicNoteAwardEmoji
_from_parent_attrs = {
"group_id": "group_id",
"epic_iid": "epic_iid",
"note_id": "id",
}
_create_attrs = RequiredOptional(required=("name",))
def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
) -> GroupEpicNoteAwardEmoji:
return cast(GroupEpicNoteAwardEmoji, super().get(id=id, lazy=lazy, **kwargs))
class ProjectIssueAwardEmoji(ObjectDeleteMixin, RESTObject):
pass
class ProjectIssueAwardEmojiManager(NoUpdateMixin, RESTManager):
_path = "/projects/{project_id}/issues/{issue_iid}/award_emoji"
_obj_cls = ProjectIssueAwardEmoji
_from_parent_attrs = {"project_id": "project_id", "issue_iid": "iid"}
_create_attrs = RequiredOptional(required=("name",))
def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
) -> ProjectIssueAwardEmoji:
return cast(ProjectIssueAwardEmoji, super().get(id=id, lazy=lazy, **kwargs))
class ProjectIssueNoteAwardEmoji(ObjectDeleteMixin, RESTObject):
pass
class ProjectIssueNoteAwardEmojiManager(NoUpdateMixin, RESTManager):
_path = "/projects/{project_id}/issues/{issue_iid}/notes/{note_id}/award_emoji"
_obj_cls = ProjectIssueNoteAwardEmoji
_from_parent_attrs = {
"project_id": "project_id",
"issue_iid": "issue_iid",
"note_id": "id",
}
_create_attrs = RequiredOptional(required=("name",))
def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
) -> ProjectIssueNoteAwardEmoji:
return cast(ProjectIssueNoteAwardEmoji, super().get(id=id, lazy=lazy, **kwargs))
class ProjectMergeRequestAwardEmoji(ObjectDeleteMixin, RESTObject):
pass
class ProjectMergeRequestAwardEmojiManager(NoUpdateMixin, RESTManager):
_path = "/projects/{project_id}/merge_requests/{mr_iid}/award_emoji"
_obj_cls = ProjectMergeRequestAwardEmoji
_from_parent_attrs = {"project_id": "project_id", "mr_iid": "iid"}
_create_attrs = RequiredOptional(required=("name",))
def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
) -> ProjectMergeRequestAwardEmoji:
return cast(
ProjectMergeRequestAwardEmoji, super().get(id=id, lazy=lazy, **kwargs)
)
class ProjectMergeRequestNoteAwardEmoji(ObjectDeleteMixin, RESTObject):
pass
class ProjectMergeRequestNoteAwardEmojiManager(NoUpdateMixin, RESTManager):
_path = "/projects/{project_id}/merge_requests/{mr_iid}/notes/{note_id}/award_emoji"
_obj_cls = ProjectMergeRequestNoteAwardEmoji
_from_parent_attrs = {
"project_id": "project_id",
"mr_iid": "mr_iid",
"note_id": "id",
}
_create_attrs = RequiredOptional(required=("name",))
def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
) -> ProjectMergeRequestNoteAwardEmoji:
return cast(
ProjectMergeRequestNoteAwardEmoji, super().get(id=id, lazy=lazy, **kwargs)
)
class ProjectSnippetAwardEmoji(ObjectDeleteMixin, RESTObject):
pass
class ProjectSnippetAwardEmojiManager(NoUpdateMixin, RESTManager):
_path = "/projects/{project_id}/snippets/{snippet_id}/award_emoji"
_obj_cls = ProjectSnippetAwardEmoji
_from_parent_attrs = {"project_id": "project_id", "snippet_id": "id"}
_create_attrs = RequiredOptional(required=("name",))
def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
) -> ProjectSnippetAwardEmoji:
return cast(ProjectSnippetAwardEmoji, super().get(id=id, lazy=lazy, **kwargs))
class ProjectSnippetNoteAwardEmoji(ObjectDeleteMixin, RESTObject):
pass
class ProjectSnippetNoteAwardEmojiManager(NoUpdateMixin, RESTManager):
_path = "/projects/{project_id}/snippets/{snippet_id}/notes/{note_id}/award_emoji"
_obj_cls = ProjectSnippetNoteAwardEmoji
_from_parent_attrs = {
"project_id": "project_id",
"snippet_id": "snippet_id",
"note_id": "id",
}
_create_attrs = RequiredOptional(required=("name",))
def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
) -> ProjectSnippetNoteAwardEmoji:
return cast(
ProjectSnippetNoteAwardEmoji, super().get(id=id, lazy=lazy, **kwargs)
)

View File

@ -0,0 +1,44 @@
from typing import Any, cast, Union
from gitlab.base import RESTManager, RESTObject
from gitlab.mixins import BadgeRenderMixin, CRUDMixin, ObjectDeleteMixin, SaveMixin
from gitlab.types import RequiredOptional
__all__ = [
"GroupBadge",
"GroupBadgeManager",
"ProjectBadge",
"ProjectBadgeManager",
]
class GroupBadge(SaveMixin, ObjectDeleteMixin, RESTObject):
pass
class GroupBadgeManager(BadgeRenderMixin, CRUDMixin, RESTManager):
_path = "/groups/{group_id}/badges"
_obj_cls = GroupBadge
_from_parent_attrs = {"group_id": "id"}
_create_attrs = RequiredOptional(required=("link_url", "image_url"))
_update_attrs = RequiredOptional(optional=("link_url", "image_url"))
def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> GroupBadge:
return cast(GroupBadge, super().get(id=id, lazy=lazy, **kwargs))
class ProjectBadge(SaveMixin, ObjectDeleteMixin, RESTObject):
pass
class ProjectBadgeManager(BadgeRenderMixin, CRUDMixin, RESTManager):
_path = "/projects/{project_id}/badges"
_obj_cls = ProjectBadge
_from_parent_attrs = {"project_id": "id"}
_create_attrs = RequiredOptional(required=("link_url", "image_url"))
_update_attrs = RequiredOptional(optional=("link_url", "image_url"))
def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
) -> ProjectBadge:
return cast(ProjectBadge, super().get(id=id, lazy=lazy, **kwargs))

View File

@ -0,0 +1,84 @@
from typing import Any, cast, Union
from gitlab.base import RESTManager, RESTObject
from gitlab.mixins import CRUDMixin, ObjectDeleteMixin, SaveMixin
from gitlab.types import RequiredOptional
__all__ = [
"GroupBoardList",
"GroupBoardListManager",
"GroupBoard",
"GroupBoardManager",
"ProjectBoardList",
"ProjectBoardListManager",
"ProjectBoard",
"ProjectBoardManager",
]
class GroupBoardList(SaveMixin, ObjectDeleteMixin, RESTObject):
pass
class GroupBoardListManager(CRUDMixin, RESTManager):
_path = "/groups/{group_id}/boards/{board_id}/lists"
_obj_cls = GroupBoardList
_from_parent_attrs = {"group_id": "group_id", "board_id": "id"}
_create_attrs = RequiredOptional(
exclusive=("label_id", "assignee_id", "milestone_id")
)
_update_attrs = RequiredOptional(required=("position",))
def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
) -> GroupBoardList:
return cast(GroupBoardList, super().get(id=id, lazy=lazy, **kwargs))
class GroupBoard(SaveMixin, ObjectDeleteMixin, RESTObject):
lists: GroupBoardListManager
class GroupBoardManager(CRUDMixin, RESTManager):
_path = "/groups/{group_id}/boards"
_obj_cls = GroupBoard
_from_parent_attrs = {"group_id": "id"}
_create_attrs = RequiredOptional(required=("name",))
def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> GroupBoard:
return cast(GroupBoard, super().get(id=id, lazy=lazy, **kwargs))
class ProjectBoardList(SaveMixin, ObjectDeleteMixin, RESTObject):
pass
class ProjectBoardListManager(CRUDMixin, RESTManager):
_path = "/projects/{project_id}/boards/{board_id}/lists"
_obj_cls = ProjectBoardList
_from_parent_attrs = {"project_id": "project_id", "board_id": "id"}
_create_attrs = RequiredOptional(
exclusive=("label_id", "assignee_id", "milestone_id")
)
_update_attrs = RequiredOptional(required=("position",))
def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
) -> ProjectBoardList:
return cast(ProjectBoardList, super().get(id=id, lazy=lazy, **kwargs))
class ProjectBoard(SaveMixin, ObjectDeleteMixin, RESTObject):
lists: ProjectBoardListManager
class ProjectBoardManager(CRUDMixin, RESTManager):
_path = "/projects/{project_id}/boards"
_obj_cls = ProjectBoard
_from_parent_attrs = {"project_id": "id"}
_create_attrs = RequiredOptional(required=("name",))
def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
) -> ProjectBoard:
return cast(ProjectBoard, super().get(id=id, lazy=lazy, **kwargs))

View File

@ -0,0 +1,63 @@
from typing import Any, cast, Union
from gitlab.base import RESTManager, RESTObject
from gitlab.mixins import (
CRUDMixin,
NoUpdateMixin,
ObjectDeleteMixin,
SaveMixin,
UpdateMethod,
)
from gitlab.types import RequiredOptional
__all__ = [
"ProjectBranch",
"ProjectBranchManager",
"ProjectProtectedBranch",
"ProjectProtectedBranchManager",
]
class ProjectBranch(ObjectDeleteMixin, RESTObject):
_id_attr = "name"
class ProjectBranchManager(NoUpdateMixin, RESTManager):
_path = "/projects/{project_id}/repository/branches"
_obj_cls = ProjectBranch
_from_parent_attrs = {"project_id": "id"}
_create_attrs = RequiredOptional(required=("branch", "ref"))
def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
) -> ProjectBranch:
return cast(ProjectBranch, super().get(id=id, lazy=lazy, **kwargs))
class ProjectProtectedBranch(SaveMixin, ObjectDeleteMixin, RESTObject):
_id_attr = "name"
class ProjectProtectedBranchManager(CRUDMixin, RESTManager):
_path = "/projects/{project_id}/protected_branches"
_obj_cls = ProjectProtectedBranch
_from_parent_attrs = {"project_id": "id"}
_create_attrs = RequiredOptional(
required=("name",),
optional=(
"push_access_level",
"merge_access_level",
"unprotect_access_level",
"allow_force_push",
"allowed_to_push",
"allowed_to_merge",
"allowed_to_unprotect",
"code_owner_approval_required",
),
)
_update_method = UpdateMethod.PATCH
def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
) -> ProjectProtectedBranch:
return cast(ProjectProtectedBranch, super().get(id=id, lazy=lazy, **kwargs))

View File

@ -0,0 +1,40 @@
from typing import Any, cast, Union
from gitlab.base import RESTManager, RESTObject
from gitlab.mixins import CRUDMixin, ObjectDeleteMixin, SaveMixin
from gitlab.types import ArrayAttribute, RequiredOptional
__all__ = [
"BroadcastMessage",
"BroadcastMessageManager",
]
class BroadcastMessage(SaveMixin, ObjectDeleteMixin, RESTObject):
pass
class BroadcastMessageManager(CRUDMixin, RESTManager):
_path = "/broadcast_messages"
_obj_cls = BroadcastMessage
_create_attrs = RequiredOptional(
required=("message",),
optional=("starts_at", "ends_at", "color", "font", "target_access_levels"),
)
_update_attrs = RequiredOptional(
optional=(
"message",
"starts_at",
"ends_at",
"color",
"font",
"target_access_levels",
)
)
_types = {"target_access_levels": ArrayAttribute}
def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
) -> BroadcastMessage:
return cast(BroadcastMessage, super().get(id=id, lazy=lazy, **kwargs))

View File

@ -0,0 +1,54 @@
from typing import Any, cast, Union
from gitlab.base import RESTManager, RESTObject
from gitlab.mixins import CreateMixin, ListMixin, RefreshMixin, RetrieveMixin
from gitlab.types import RequiredOptional
__all__ = [
"BulkImport",
"BulkImportManager",
"BulkImportAllEntity",
"BulkImportAllEntityManager",
"BulkImportEntity",
"BulkImportEntityManager",
]
class BulkImport(RefreshMixin, RESTObject):
entities: "BulkImportEntityManager"
class BulkImportManager(CreateMixin, RetrieveMixin, RESTManager):
_path = "/bulk_imports"
_obj_cls = BulkImport
_create_attrs = RequiredOptional(required=("configuration", "entities"))
_list_filters = ("sort", "status")
def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> BulkImport:
return cast(BulkImport, super().get(id=id, lazy=lazy, **kwargs))
class BulkImportEntity(RefreshMixin, RESTObject):
pass
class BulkImportEntityManager(RetrieveMixin, RESTManager):
_path = "/bulk_imports/{bulk_import_id}/entities"
_obj_cls = BulkImportEntity
_from_parent_attrs = {"bulk_import_id": "id"}
_list_filters = ("sort", "status")
def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
) -> BulkImportEntity:
return cast(BulkImportEntity, super().get(id=id, lazy=lazy, **kwargs))
class BulkImportAllEntity(RESTObject):
pass
class BulkImportAllEntityManager(ListMixin, RESTManager):
_path = "/bulk_imports/entities"
_obj_cls = BulkImportAllEntity
_list_filters = ("sort", "status")

View File

@ -0,0 +1,78 @@
"""
GitLab API:
https://docs.gitlab.com/ee/api/lint.html
"""
from typing import Any, cast
from gitlab.base import RESTManager, RESTObject
from gitlab.cli import register_custom_action
from gitlab.exceptions import GitlabCiLintError
from gitlab.mixins import CreateMixin, GetWithoutIdMixin
from gitlab.types import RequiredOptional
__all__ = [
"CiLint",
"CiLintManager",
"ProjectCiLint",
"ProjectCiLintManager",
]
class CiLint(RESTObject):
_id_attr = None
class CiLintManager(CreateMixin, RESTManager):
_path = "/ci/lint"
_obj_cls = CiLint
_create_attrs = RequiredOptional(
required=("content",), optional=("include_merged_yaml", "include_jobs")
)
@register_custom_action(
cls_names="CiLintManager",
required=("content",),
optional=("include_merged_yaml", "include_jobs"),
)
def validate(self, *args: Any, **kwargs: Any) -> None:
"""Raise an error if the CI Lint results are not valid.
This is a custom python-gitlab method to wrap lint endpoints."""
result = self.create(*args, **kwargs)
if result.status != "valid":
message = ",\n".join(result.errors)
raise GitlabCiLintError(message)
class ProjectCiLint(RESTObject):
_id_attr = None
class ProjectCiLintManager(GetWithoutIdMixin, CreateMixin, RESTManager):
_path = "/projects/{project_id}/ci/lint"
_obj_cls = ProjectCiLint
_from_parent_attrs = {"project_id": "id"}
_optional_get_attrs = ("dry_run", "include_jobs", "ref")
_create_attrs = RequiredOptional(
required=("content",), optional=("dry_run", "include_jobs", "ref")
)
def get(self, **kwargs: Any) -> ProjectCiLint:
return cast(ProjectCiLint, super().get(**kwargs))
@register_custom_action(
cls_names="ProjectCiLintManager",
required=("content",),
optional=("dry_run", "include_jobs", "ref"),
)
def validate(self, *args: Any, **kwargs: Any) -> None:
"""Raise an error if the Project CI Lint results are not valid.
This is a custom python-gitlab method to wrap lint endpoints."""
result = self.create(*args, **kwargs)
if not result.valid:
message = ",\n".join(result.errors)
raise GitlabCiLintError(message)

View File

@ -0,0 +1,26 @@
from typing import Any, cast, Union
from gitlab.base import RESTManager, RESTObject
from gitlab.mixins import NoUpdateMixin, ObjectDeleteMixin, SaveMixin
from gitlab.types import RequiredOptional
__all__ = [
"ProjectClusterAgent",
"ProjectClusterAgentManager",
]
class ProjectClusterAgent(SaveMixin, ObjectDeleteMixin, RESTObject):
_repr_attr = "name"
class ProjectClusterAgentManager(NoUpdateMixin, RESTManager):
_path = "/projects/{project_id}/cluster_agents"
_obj_cls = ProjectClusterAgent
_from_parent_attrs = {"project_id": "id"}
_create_attrs = RequiredOptional(required=("name",))
def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
) -> ProjectClusterAgent:
return cast(ProjectClusterAgent, super().get(id=id, lazy=lazy, **kwargs))

View File

@ -0,0 +1,115 @@
from typing import Any, cast, Dict, Optional, Union
from gitlab import exceptions as exc
from gitlab.base import RESTManager, RESTObject
from gitlab.mixins import CreateMixin, CRUDMixin, ObjectDeleteMixin, SaveMixin
from gitlab.types import RequiredOptional
__all__ = [
"GroupCluster",
"GroupClusterManager",
"ProjectCluster",
"ProjectClusterManager",
]
class GroupCluster(SaveMixin, ObjectDeleteMixin, RESTObject):
pass
class GroupClusterManager(CRUDMixin, RESTManager):
_path = "/groups/{group_id}/clusters"
_obj_cls = GroupCluster
_from_parent_attrs = {"group_id": "id"}
_create_attrs = RequiredOptional(
required=("name", "platform_kubernetes_attributes"),
optional=("domain", "enabled", "managed", "environment_scope"),
)
_update_attrs = RequiredOptional(
optional=(
"name",
"domain",
"management_project_id",
"platform_kubernetes_attributes",
"environment_scope",
),
)
@exc.on_http_error(exc.GitlabStopError)
def create(
self, data: Optional[Dict[str, Any]] = None, **kwargs: Any
) -> GroupCluster:
"""Create a new object.
Args:
data: Parameters to send to the server to create the
resource
**kwargs: Extra options to send to the server (e.g. sudo or
'ref_name', 'stage', 'name', 'all')
Raises:
GitlabAuthenticationError: If authentication is not correct
GitlabCreateError: If the server cannot perform the request
Returns:
A new instance of the manage object class build with
the data sent by the server
"""
path = f"{self.path}/user"
return cast(GroupCluster, CreateMixin.create(self, data, path=path, **kwargs))
def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
) -> GroupCluster:
return cast(GroupCluster, super().get(id=id, lazy=lazy, **kwargs))
class ProjectCluster(SaveMixin, ObjectDeleteMixin, RESTObject):
pass
class ProjectClusterManager(CRUDMixin, RESTManager):
_path = "/projects/{project_id}/clusters"
_obj_cls = ProjectCluster
_from_parent_attrs = {"project_id": "id"}
_create_attrs = RequiredOptional(
required=("name", "platform_kubernetes_attributes"),
optional=("domain", "enabled", "managed", "environment_scope"),
)
_update_attrs = RequiredOptional(
optional=(
"name",
"domain",
"management_project_id",
"platform_kubernetes_attributes",
"environment_scope",
),
)
@exc.on_http_error(exc.GitlabStopError)
def create(
self, data: Optional[Dict[str, Any]] = None, **kwargs: Any
) -> ProjectCluster:
"""Create a new object.
Args:
data: Parameters to send to the server to create the
resource
**kwargs: Extra options to send to the server (e.g. sudo or
'ref_name', 'stage', 'name', 'all')
Raises:
GitlabAuthenticationError: If authentication is not correct
GitlabCreateError: If the server cannot perform the request
Returns:
A new instance of the manage object class build with
the data sent by the server
"""
path = f"{self.path}/user"
return cast(ProjectCluster, CreateMixin.create(self, data, path=path, **kwargs))
def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
) -> ProjectCluster:
return cast(ProjectCluster, super().get(id=id, lazy=lazy, **kwargs))

View File

@ -0,0 +1,253 @@
from typing import Any, cast, Dict, List, Optional, TYPE_CHECKING, Union
import requests
import gitlab
from gitlab import cli
from gitlab import exceptions as exc
from gitlab.base import RESTManager, RESTObject
from gitlab.mixins import CreateMixin, ListMixin, RefreshMixin, RetrieveMixin
from gitlab.types import RequiredOptional
from .discussions import ProjectCommitDiscussionManager # noqa: F401
__all__ = [
"ProjectCommit",
"ProjectCommitManager",
"ProjectCommitComment",
"ProjectCommitCommentManager",
"ProjectCommitStatus",
"ProjectCommitStatusManager",
]
class ProjectCommit(RESTObject):
_repr_attr = "title"
comments: "ProjectCommitCommentManager"
discussions: ProjectCommitDiscussionManager
statuses: "ProjectCommitStatusManager"
@cli.register_custom_action(cls_names="ProjectCommit")
@exc.on_http_error(exc.GitlabGetError)
def diff(self, **kwargs: Any) -> Union[gitlab.GitlabList, List[Dict[str, Any]]]:
"""Generate the commit diff.
Args:
**kwargs: Extra options to send to the server (e.g. sudo)
Raises:
GitlabAuthenticationError: If authentication is not correct
GitlabGetError: If the diff could not be retrieved
Returns:
The changes done in this commit
"""
path = f"{self.manager.path}/{self.encoded_id}/diff"
return self.manager.gitlab.http_list(path, **kwargs)
@cli.register_custom_action(cls_names="ProjectCommit", required=("branch",))
@exc.on_http_error(exc.GitlabCherryPickError)
def cherry_pick(self, branch: str, **kwargs: Any) -> None:
"""Cherry-pick a commit into a branch.
Args:
branch: Name of target branch
**kwargs: Extra options to send to the server (e.g. sudo)
Raises:
GitlabAuthenticationError: If authentication is not correct
GitlabCherryPickError: If the cherry-pick could not be performed
"""
path = f"{self.manager.path}/{self.encoded_id}/cherry_pick"
post_data = {"branch": branch}
self.manager.gitlab.http_post(path, post_data=post_data, **kwargs)
@cli.register_custom_action(cls_names="ProjectCommit", optional=("type",))
@exc.on_http_error(exc.GitlabGetError)
def refs(
self, type: str = "all", **kwargs: Any
) -> Union[gitlab.GitlabList, List[Dict[str, Any]]]:
"""List the references the commit is pushed to.
Args:
type: The scope of references ('branch', 'tag' or 'all')
**kwargs: Extra options to send to the server (e.g. sudo)
Raises:
GitlabAuthenticationError: If authentication is not correct
GitlabGetError: If the references could not be retrieved
Returns:
The references the commit is pushed to.
"""
path = f"{self.manager.path}/{self.encoded_id}/refs"
query_data = {"type": type}
return self.manager.gitlab.http_list(path, query_data=query_data, **kwargs)
@cli.register_custom_action(cls_names="ProjectCommit")
@exc.on_http_error(exc.GitlabGetError)
def merge_requests(
self, **kwargs: Any
) -> Union[gitlab.GitlabList, List[Dict[str, Any]]]:
"""List the merge requests related to the commit.
Args:
**kwargs: Extra options to send to the server (e.g. sudo)
Raises:
GitlabAuthenticationError: If authentication is not correct
GitlabGetError: If the references could not be retrieved
Returns:
The merge requests related to the commit.
"""
path = f"{self.manager.path}/{self.encoded_id}/merge_requests"
return self.manager.gitlab.http_list(path, **kwargs)
@cli.register_custom_action(cls_names="ProjectCommit", required=("branch",))
@exc.on_http_error(exc.GitlabRevertError)
def revert(
self, branch: str, **kwargs: Any
) -> Union[Dict[str, Any], requests.Response]:
"""Revert a commit on a given branch.
Args:
branch: Name of target branch
**kwargs: Extra options to send to the server (e.g. sudo)
Raises:
GitlabAuthenticationError: If authentication is not correct
GitlabRevertError: If the revert could not be performed
Returns:
The new commit data (*not* a RESTObject)
"""
path = f"{self.manager.path}/{self.encoded_id}/revert"
post_data = {"branch": branch}
return self.manager.gitlab.http_post(path, post_data=post_data, **kwargs)
@cli.register_custom_action(cls_names="ProjectCommit")
@exc.on_http_error(exc.GitlabGetError)
def sequence(self, **kwargs: Any) -> Union[Dict[str, Any], requests.Response]:
"""Get the sequence number of the commit.
Args:
**kwargs: Extra options to send to the server (e.g. sudo)
Raises:
GitlabAuthenticationError: If authentication is not correct
GitlabGetError: If the sequence number could not be retrieved
Returns:
The commit's sequence number
"""
path = f"{self.manager.path}/{self.encoded_id}/sequence"
return self.manager.gitlab.http_get(path, **kwargs)
@cli.register_custom_action(cls_names="ProjectCommit")
@exc.on_http_error(exc.GitlabGetError)
def signature(self, **kwargs: Any) -> Union[Dict[str, Any], requests.Response]:
"""Get the signature of the commit.
Args:
**kwargs: Extra options to send to the server (e.g. sudo)
Raises:
GitlabAuthenticationError: If authentication is not correct
GitlabGetError: If the signature could not be retrieved
Returns:
The commit's signature data
"""
path = f"{self.manager.path}/{self.encoded_id}/signature"
return self.manager.gitlab.http_get(path, **kwargs)
class ProjectCommitManager(RetrieveMixin, CreateMixin, RESTManager):
_path = "/projects/{project_id}/repository/commits"
_obj_cls = ProjectCommit
_from_parent_attrs = {"project_id": "id"}
_create_attrs = RequiredOptional(
required=("branch", "commit_message", "actions"),
optional=("author_email", "author_name"),
)
_list_filters = (
"all",
"ref_name",
"since",
"until",
"path",
"with_stats",
"first_parent",
"order",
"trailers",
)
def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
) -> ProjectCommit:
return cast(ProjectCommit, super().get(id=id, lazy=lazy, **kwargs))
class ProjectCommitComment(RESTObject):
_id_attr = None
_repr_attr = "note"
class ProjectCommitCommentManager(ListMixin, CreateMixin, RESTManager):
_path = "/projects/{project_id}/repository/commits/{commit_id}/comments"
_obj_cls = ProjectCommitComment
_from_parent_attrs = {"project_id": "project_id", "commit_id": "id"}
_create_attrs = RequiredOptional(
required=("note",), optional=("path", "line", "line_type")
)
class ProjectCommitStatus(RefreshMixin, RESTObject):
pass
class ProjectCommitStatusManager(ListMixin, CreateMixin, RESTManager):
_path = "/projects/{project_id}/repository/commits/{commit_id}/statuses"
_obj_cls = ProjectCommitStatus
_from_parent_attrs = {"project_id": "project_id", "commit_id": "id"}
_create_attrs = RequiredOptional(
required=("state",),
optional=("description", "name", "context", "ref", "target_url", "coverage"),
)
@exc.on_http_error(exc.GitlabCreateError)
def create(
self, data: Optional[Dict[str, Any]] = None, **kwargs: Any
) -> ProjectCommitStatus:
"""Create a new object.
Args:
data: Parameters to send to the server to create the
resource
**kwargs: Extra options to send to the server (e.g. sudo or
'ref_name', 'stage', 'name', 'all')
Raises:
GitlabAuthenticationError: If authentication is not correct
GitlabCreateError: If the server cannot perform the request
Returns:
A new instance of the manage object class build with
the data sent by the server
"""
# project_id and commit_id are in the data dict when using the CLI, but
# they are missing when using only the API
# See #511
base_path = "/projects/{project_id}/statuses/{commit_id}"
path: Optional[str]
if data is not None and "project_id" in data and "commit_id" in data:
path = base_path.format(**data)
else:
path = self._compute_path(base_path)
if TYPE_CHECKING:
assert path is not None
return cast(
ProjectCommitStatus, CreateMixin.create(self, data, path=path, **kwargs)
)

View File

@ -0,0 +1,96 @@
from typing import Any, cast, TYPE_CHECKING, Union
from gitlab import cli
from gitlab import exceptions as exc
from gitlab.base import RESTManager, RESTObject
from gitlab.mixins import (
DeleteMixin,
GetMixin,
ListMixin,
ObjectDeleteMixin,
RetrieveMixin,
)
__all__ = [
"GroupRegistryRepositoryManager",
"ProjectRegistryRepository",
"ProjectRegistryRepositoryManager",
"ProjectRegistryTag",
"ProjectRegistryTagManager",
"RegistryRepository",
"RegistryRepositoryManager",
]
class ProjectRegistryRepository(ObjectDeleteMixin, RESTObject):
tags: "ProjectRegistryTagManager"
class ProjectRegistryRepositoryManager(DeleteMixin, ListMixin, RESTManager):
_path = "/projects/{project_id}/registry/repositories"
_obj_cls = ProjectRegistryRepository
_from_parent_attrs = {"project_id": "id"}
class ProjectRegistryTag(ObjectDeleteMixin, RESTObject):
_id_attr = "name"
class ProjectRegistryTagManager(DeleteMixin, RetrieveMixin, RESTManager):
_obj_cls = ProjectRegistryTag
_from_parent_attrs = {"project_id": "project_id", "repository_id": "id"}
_path = "/projects/{project_id}/registry/repositories/{repository_id}/tags"
@cli.register_custom_action(
cls_names="ProjectRegistryTagManager",
required=("name_regex_delete",),
optional=("keep_n", "name_regex_keep", "older_than"),
)
@exc.on_http_error(exc.GitlabDeleteError)
def delete_in_bulk(self, name_regex_delete: str, **kwargs: Any) -> None:
"""Delete Tag in bulk
Args:
name_regex_delete: The regex of the name to delete. To delete all
tags specify .*.
keep_n: The amount of latest tags of given name to keep.
name_regex_keep: The regex of the name to keep. This value
overrides any matches from name_regex.
older_than: Tags to delete that are older than the given time,
written in human readable form 1h, 1d, 1month.
**kwargs: Extra options to send to the server (e.g. sudo)
Raises:
GitlabAuthenticationError: If authentication is not correct
GitlabDeleteError: If the server cannot perform the request
"""
valid_attrs = ["keep_n", "name_regex_keep", "older_than"]
data = {"name_regex_delete": name_regex_delete}
data.update({k: v for k, v in kwargs.items() if k in valid_attrs})
if TYPE_CHECKING:
assert self.path is not None
self.gitlab.http_delete(self.path, query_data=data, **kwargs)
def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
) -> ProjectRegistryTag:
return cast(ProjectRegistryTag, super().get(id=id, lazy=lazy, **kwargs))
class GroupRegistryRepositoryManager(ListMixin, RESTManager):
_path = "/groups/{group_id}/registry/repositories"
_obj_cls = ProjectRegistryRepository
_from_parent_attrs = {"group_id": "id"}
class RegistryRepository(RESTObject):
_repr_attr = "path"
class RegistryRepositoryManager(GetMixin, RESTManager):
_path = "/registry/repositories"
_obj_cls = RegistryRepository
def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
) -> RegistryRepository:
return cast(RegistryRepository, super().get(id=id, lazy=lazy, **kwargs))

View File

@ -0,0 +1,58 @@
from typing import Any, cast, Union
from gitlab.base import RESTManager, RESTObject
from gitlab.mixins import DeleteMixin, ObjectDeleteMixin, RetrieveMixin, SetMixin
__all__ = [
"GroupCustomAttribute",
"GroupCustomAttributeManager",
"ProjectCustomAttribute",
"ProjectCustomAttributeManager",
"UserCustomAttribute",
"UserCustomAttributeManager",
]
class GroupCustomAttribute(ObjectDeleteMixin, RESTObject):
_id_attr = "key"
class GroupCustomAttributeManager(RetrieveMixin, SetMixin, DeleteMixin, RESTManager):
_path = "/groups/{group_id}/custom_attributes"
_obj_cls = GroupCustomAttribute
_from_parent_attrs = {"group_id": "id"}
def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
) -> GroupCustomAttribute:
return cast(GroupCustomAttribute, super().get(id=id, lazy=lazy, **kwargs))
class ProjectCustomAttribute(ObjectDeleteMixin, RESTObject):
_id_attr = "key"
class ProjectCustomAttributeManager(RetrieveMixin, SetMixin, DeleteMixin, RESTManager):
_path = "/projects/{project_id}/custom_attributes"
_obj_cls = ProjectCustomAttribute
_from_parent_attrs = {"project_id": "id"}
def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
) -> ProjectCustomAttribute:
return cast(ProjectCustomAttribute, super().get(id=id, lazy=lazy, **kwargs))
class UserCustomAttribute(ObjectDeleteMixin, RESTObject):
_id_attr = "key"
class UserCustomAttributeManager(RetrieveMixin, SetMixin, DeleteMixin, RESTManager):
_path = "/users/{user_id}/custom_attributes"
_obj_cls = UserCustomAttribute
_from_parent_attrs = {"user_id": "id"}
def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
) -> UserCustomAttribute:
return cast(UserCustomAttribute, super().get(id=id, lazy=lazy, **kwargs))

Some files were not shown because too many files have changed in this diff Show More