From c02dabd25507a14d666e85c7f1ea7831c64d0394 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sat, 20 May 2017 10:24:08 +0200 Subject: [PATCH 01/32] Initial, non-functional v4 support --- gitlab/__init__.py | 10 ++++++---- gitlab/config.py | 9 +++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/gitlab/__init__.py b/gitlab/__init__.py index 1db03b0ac..edefd89b7 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -69,9 +69,10 @@ class Gitlab(object): def __init__(self, url, private_token=None, email=None, password=None, ssl_verify=True, http_username=None, http_password=None, - timeout=None): + timeout=None, api_version='3'): - self._url = '%s/api/v3' % url + self._api_version = str(api_version) + self._url = '%s/api/v%s' % (url, api_version) #: Timeout to use for requests to gitlab server self.timeout = timeout #: Headers that will be used in request to GitLab @@ -152,7 +153,8 @@ def from_config(gitlab_id=None, config_files=None): return Gitlab(config.url, private_token=config.token, ssl_verify=config.ssl_verify, timeout=config.timeout, http_username=config.http_username, - http_password=config.http_password) + http_password=config.http_password, + api_version=config.api_version) def auth(self): """Performs an authentication. @@ -212,7 +214,7 @@ def set_url(self, url): Args: url (str): Base URL of the GitLab server. """ - self._url = '%s/api/v3' % url + self._url = '%s/api/v%s' % (url, self._api_version) def _construct_url(self, id_, obj, parameters, action=None): if 'next_url' in parameters: diff --git a/gitlab/config.py b/gitlab/config.py index 3ef2efb03..9af804dd2 100644 --- a/gitlab/config.py +++ b/gitlab/config.py @@ -88,3 +88,12 @@ def __init__(self, gitlab_id=None, config_files=None): 'http_password') except Exception: pass + + self.api_version = '3' + try: + self.api_version = self._config.get(self.gitlab_id, 'api_version') + except Exception: + pass + if self.api_version not in ('3', '4'): + raise GitlabDataError("Unsupported API version: %s" % + self.api_version) From deecf1769ed4d3e9e2674412559413eb058494cf Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Tue, 23 May 2017 09:10:44 +0200 Subject: [PATCH 02/32] [v4] Update project search API * projects.search() is not implemented in v4 * add the 'search' attribute to projects.list() --- docs/gl_objects/projects.py | 2 +- gitlab/__init__.py | 4 ++++ gitlab/objects.py | 6 ++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/gl_objects/projects.py b/docs/gl_objects/projects.py index 8e5cb1332..2f8d5b5b2 100644 --- a/docs/gl_objects/projects.py +++ b/docs/gl_objects/projects.py @@ -16,7 +16,7 @@ projects = gl.projects.all() # Search projects -projects = gl.projects.list(search='query') +projects = gl.projects.list(search='keyword') # end list # get diff --git a/gitlab/__init__.py b/gitlab/__init__.py index edefd89b7..19da2c7c2 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -126,6 +126,10 @@ def __init__(self, url, private_token=None, email=None, password=None, manager = cls(self) setattr(self, var_name, manager) + @property + def api_version(self): + return self._api_version + def _cls_to_manager_prefix(self, cls): # Manage bad naming decisions camel_case = (cls.__name__ diff --git a/gitlab/objects.py b/gitlab/objects.py index 0def183d6..630d41584 100644 --- a/gitlab/objects.py +++ b/gitlab/objects.py @@ -2278,6 +2278,7 @@ class Project(GitlabObject): _constructorTypes = {'owner': 'User', 'namespace': 'Group'} optionalListAttrs = ['search'] requiredCreateAttrs = ['name'] + optionalListAttrs = ['search'] optionalCreateAttrs = ['path', 'namespace_id', 'description', 'issues_enabled', 'merge_requests_enabled', 'builds_enabled', 'wiki_enabled', @@ -2678,6 +2679,8 @@ class ProjectManager(BaseManager): def search(self, query, **kwargs): """Search projects by name. + API v3 only. + .. note:: The search is only performed on the project name (not on the @@ -2696,6 +2699,9 @@ def search(self, query, **kwargs): Returns: list(gitlab.Gitlab.Project): A list of matching projects. """ + if self.gitlab.api_version == '4': + raise NotImplementedError("Not supported by v4 API") + return self.gitlab._raw_list("/projects/search/" + query, Project, **kwargs) From f3738854f0d010bade44edc60404dbab984d2adb Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Tue, 23 May 2017 15:44:06 +0200 Subject: [PATCH 03/32] Update Gitlab __init__ docstring --- gitlab/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gitlab/__init__.py b/gitlab/__init__.py index 19da2c7c2..10913193d 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -65,6 +65,7 @@ class Gitlab(object): timeout (float): Timeout to use for requests to the GitLab server. http_username (str): Username for HTTP authentication http_password (str): Password for HTTP authentication + api_version (str): Gitlab API version to use (3 or 4) """ def __init__(self, url, private_token=None, email=None, password=None, From e853a30b0c083fa835513a82816b315cf147092c Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Tue, 23 May 2017 21:11:17 +0200 Subject: [PATCH 04/32] Reorganise the code to handle v3 and v4 objects Having objects managing both versions will only make the code more complicated, with lots of tests everywhere. This solution might generate some code duplication, but it should be maintainable. --- gitlab/__init__.py | 54 ++-- gitlab/tests/test_manager.py | 2 +- gitlab/v3/__init__.py | 0 gitlab/{ => v3}/objects.py | 598 +++-------------------------------- gitlab/v4/__init__.py | 0 gitlab/v4/objects.py | 18 ++ tools/python_test.py | 2 +- 7 files changed, 98 insertions(+), 576 deletions(-) create mode 100644 gitlab/v3/__init__.py rename gitlab/{ => v3}/objects.py (79%) create mode 100644 gitlab/v4/__init__.py create mode 100644 gitlab/v4/objects.py diff --git a/gitlab/__init__.py b/gitlab/__init__.py index 10913193d..b3f6dcd15 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -19,6 +19,7 @@ from __future__ import print_function from __future__ import division from __future__ import absolute_import +import importlib import inspect import itertools import json @@ -31,7 +32,7 @@ import gitlab.config from gitlab.const import * # noqa from gitlab.exceptions import * # noqa -from gitlab.objects import * # noqa +from gitlab.v3.objects import * # noqa __title__ = 'python-gitlab' __version__ = '0.20' @@ -91,40 +92,43 @@ def __init__(self, url, private_token=None, email=None, password=None, #: Create a session object for requests self.session = requests.Session() - self.broadcastmessages = BroadcastMessageManager(self) - self.keys = KeyManager(self) - self.deploykeys = DeployKeyManager(self) - self.gitlabciymls = GitlabciymlManager(self) - self.gitignores = GitignoreManager(self) - self.groups = GroupManager(self) - self.hooks = HookManager(self) - self.issues = IssueManager(self) - self.licenses = LicenseManager(self) - self.namespaces = NamespaceManager(self) - self.notificationsettings = NotificationSettingsManager(self) - self.projects = ProjectManager(self) - self.runners = RunnerManager(self) - self.settings = ApplicationSettingsManager(self) - self.sidekiq = SidekiqManager(self) - self.snippets = SnippetManager(self) - self.users = UserManager(self) - self.teams = TeamManager(self) - self.todos = TodoManager(self) + objects = importlib.import_module('gitlab.v%s.objects' % + self._api_version) + + self.broadcastmessages = objects.BroadcastMessageManager(self) + self.keys = objects.KeyManager(self) + self.deploykeys = objects.DeployKeyManager(self) + self.gitlabciymls = objects.GitlabciymlManager(self) + self.gitignores = objects.GitignoreManager(self) + self.groups = objects.GroupManager(self) + self.hooks = objects.HookManager(self) + self.issues = objects.IssueManager(self) + self.licenses = objects.LicenseManager(self) + self.namespaces = objects.NamespaceManager(self) + self.notificationsettings = objects.NotificationSettingsManager(self) + self.projects = objects.ProjectManager(self) + self.runners = objects.RunnerManager(self) + self.settings = objects.ApplicationSettingsManager(self) + self.sidekiq = objects.SidekiqManager(self) + self.snippets = objects.SnippetManager(self) + self.users = objects.UserManager(self) + self.teams = objects.TeamManager(self) + self.todos = objects.TodoManager(self) # build the "submanagers" - for parent_cls in six.itervalues(globals()): + for parent_cls in six.itervalues(vars(objects)): if (not inspect.isclass(parent_cls) - or not issubclass(parent_cls, GitlabObject) - or parent_cls == CurrentUser): + or not issubclass(parent_cls, objects.GitlabObject) + or parent_cls == objects.CurrentUser): continue if not parent_cls.managers: continue - for var, cls, attrs in parent_cls.managers: + for var, cls_name, attrs in parent_cls.managers: var_name = '%s_%s' % (self._cls_to_manager_prefix(parent_cls), var) - manager = cls(self) + manager = getattr(objects, cls_name)(self) setattr(self, var_name, manager) @property diff --git a/gitlab/tests/test_manager.py b/gitlab/tests/test_manager.py index 16e13f2af..4f4dbe1b3 100644 --- a/gitlab/tests/test_manager.py +++ b/gitlab/tests/test_manager.py @@ -25,7 +25,7 @@ from httmock import urlmatch # noqa from gitlab import * # noqa -from gitlab.objects import BaseManager # noqa +from gitlab.v3.objects import BaseManager # noqa class FakeChildObject(GitlabObject): diff --git a/gitlab/v3/__init__.py b/gitlab/v3/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/gitlab/objects.py b/gitlab/v3/objects.py similarity index 79% rename from gitlab/objects.py rename to gitlab/v3/objects.py index 630d41584..01bb67040 100644 --- a/gitlab/objects.py +++ b/gitlab/v3/objects.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2013-2015 Gauvain Pocentek +# Copyright (C) 2013-2017 Gauvain Pocentek # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by @@ -19,519 +19,18 @@ from __future__ import division from __future__ import absolute_import import base64 -import copy -import itertools import json -import sys import urllib import warnings import six import gitlab +from gitlab.base import * # noqa from gitlab.exceptions import * # noqa from gitlab import utils -class jsonEncoder(json.JSONEncoder): - def default(self, obj): - if isinstance(obj, GitlabObject): - return obj.as_dict() - elif isinstance(obj, gitlab.Gitlab): - return {'url': obj._url} - return json.JSONEncoder.default(self, obj) - - -class BaseManager(object): - """Base manager class for API operations. - - Managers provide method to manage GitLab API objects, such as retrieval, - listing, creation. - - Inherited class must define the ``obj_cls`` attribute. - - Attributes: - obj_cls (class): class of objects wrapped by this manager. - """ - - obj_cls = None - - def __init__(self, gl, parent=None, args=[]): - """Constructs a manager. - - Args: - gl (gitlab.Gitlab): Gitlab object referencing the GitLab server. - parent (Optional[Manager]): A parent manager. - args (list): A list of tuples defining a link between the - parent/child attributes. - - Raises: - AttributeError: If `obj_cls` is None. - """ - self.gitlab = gl - self.args = args - self.parent = parent - - if self.obj_cls is None: - raise AttributeError("obj_cls must be defined") - - def _set_parent_args(self, **kwargs): - args = copy.copy(kwargs) - if self.parent is not None: - for attr, parent_attr in self.args: - args.setdefault(attr, getattr(self.parent, parent_attr)) - - return args - - def get(self, id=None, **kwargs): - """Get a GitLab object. - - Args: - id: ID of the object to retrieve. - **kwargs: Additional arguments to send to GitLab. - - Returns: - object: An object of class `obj_cls`. - - Raises: - NotImplementedError: If objects cannot be retrieved. - GitlabGetError: If the server fails to perform the request. - """ - args = self._set_parent_args(**kwargs) - if not self.obj_cls.canGet: - raise NotImplementedError - if id is None and self.obj_cls.getRequiresId is True: - raise ValueError('The id argument must be defined.') - return self.obj_cls.get(self.gitlab, id, **args) - - def list(self, **kwargs): - """Get a list of GitLab objects. - - Args: - **kwargs: Additional arguments to send to GitLab. - - Returns: - list[object]: A list of `obj_cls` objects. - - Raises: - NotImplementedError: If objects cannot be listed. - GitlabListError: If the server fails to perform the request. - """ - args = self._set_parent_args(**kwargs) - if not self.obj_cls.canList: - raise NotImplementedError - return self.obj_cls.list(self.gitlab, **args) - - def create(self, data, **kwargs): - """Create a new object of class `obj_cls`. - - Args: - data (dict): The parameters to send to the GitLab server to create - the object. Required and optional arguments are defined in the - `requiredCreateAttrs` and `optionalCreateAttrs` of the - `obj_cls` class. - **kwargs: Additional arguments to send to GitLab. - - Returns: - object: A newly create `obj_cls` object. - - Raises: - NotImplementedError: If objects cannot be created. - GitlabCreateError: If the server fails to perform the request. - """ - args = self._set_parent_args(**kwargs) - if not self.obj_cls.canCreate: - raise NotImplementedError - return self.obj_cls.create(self.gitlab, data, **args) - - def delete(self, id, **kwargs): - """Delete a GitLab object. - - Args: - id: ID of the object to delete. - - Raises: - NotImplementedError: If objects cannot be deleted. - GitlabDeleteError: If the server fails to perform the request. - """ - args = self._set_parent_args(**kwargs) - if not self.obj_cls.canDelete: - raise NotImplementedError - self.gitlab.delete(self.obj_cls, id, **args) - - -class GitlabObject(object): - """Base class for all classes that interface with GitLab.""" - #: Url to use in GitLab for this object - _url = None - # Some objects (e.g. merge requests) have different urls for singular and - # plural - _urlPlural = None - _id_in_delete_url = True - _id_in_update_url = True - _constructorTypes = None - - #: Tells if GitLab-api allows retrieving single objects. - canGet = True - #: Tells if GitLab-api allows listing of objects. - canList = True - #: Tells if GitLab-api allows creation of new objects. - canCreate = True - #: Tells if GitLab-api allows updating object. - canUpdate = True - #: Tells if GitLab-api allows deleting object. - canDelete = True - #: Attributes that are required for constructing url. - requiredUrlAttrs = [] - #: Attributes that are required when retrieving list of objects. - requiredListAttrs = [] - #: Attributes that are optional when retrieving list of objects. - optionalListAttrs = [] - #: Attributes that are optional when retrieving single object. - optionalGetAttrs = [] - #: Attributes that are required when retrieving single object. - requiredGetAttrs = [] - #: Attributes that are required when deleting object. - requiredDeleteAttrs = [] - #: Attributes that are required when creating a new object. - requiredCreateAttrs = [] - #: Attributes that are optional when creating a new object. - optionalCreateAttrs = [] - #: Attributes that are required when updating an object. - requiredUpdateAttrs = [] - #: Attributes that are optional when updating an object. - optionalUpdateAttrs = [] - #: Whether the object ID is required in the GET url. - getRequiresId = True - #: List of managers to create. - managers = [] - #: Name of the identifier of an object. - idAttr = 'id' - #: Attribute to use as ID when displaying the object. - shortPrintAttr = None - - def _data_for_gitlab(self, extra_parameters={}, update=False, - as_json=True): - data = {} - if update and (self.requiredUpdateAttrs or self.optionalUpdateAttrs): - attributes = itertools.chain(self.requiredUpdateAttrs, - self.optionalUpdateAttrs) - else: - attributes = itertools.chain(self.requiredCreateAttrs, - self.optionalCreateAttrs) - attributes = list(attributes) + ['sudo', 'page', 'per_page'] - for attribute in attributes: - if hasattr(self, attribute): - value = getattr(self, attribute) - # labels need to be sent as a comma-separated list - if attribute == 'labels' and isinstance(value, list): - value = ", ".join(value) - elif attribute == 'sudo': - value = str(value) - data[attribute] = value - - data.update(extra_parameters) - - return json.dumps(data) if as_json else data - - @classmethod - def list(cls, gl, **kwargs): - """Retrieve a list of objects from GitLab. - - Args: - gl (gitlab.Gitlab): Gitlab object referencing the GitLab server. - per_page (int): Maximum number of items to return. - page (int): ID of the page to return when using pagination. - - Returns: - list[object]: A list of objects. - - Raises: - NotImplementedError: If objects can't be listed. - GitlabListError: If the server cannot perform the request. - """ - if not cls.canList: - raise NotImplementedError - - if not cls._url: - raise NotImplementedError - - return gl.list(cls, **kwargs) - - @classmethod - def get(cls, gl, id, **kwargs): - """Retrieve a single object. - - Args: - gl (gitlab.Gitlab): Gitlab object referencing the GitLab server. - id (int or str): ID of the object to retrieve. - - Returns: - object: The found GitLab object. - - Raises: - NotImplementedError: If objects can't be retrieved. - GitlabGetError: If the server cannot perform the request. - """ - - if cls.canGet is False: - raise NotImplementedError - elif cls.canGet is True: - return cls(gl, id, **kwargs) - elif cls.canGet == 'from_list': - for obj in cls.list(gl, **kwargs): - obj_id = getattr(obj, obj.idAttr) - if str(obj_id) == str(id): - return obj - - raise GitlabGetError("Object not found") - - def _get_object(self, k, v, **kwargs): - if self._constructorTypes and k in self._constructorTypes: - return globals()[self._constructorTypes[k]](self.gitlab, v, - **kwargs) - else: - return v - - def _set_from_dict(self, data, **kwargs): - if not hasattr(data, 'items'): - return - - for k, v in data.items(): - # If a k attribute already exists and is a Manager, do nothing (see - # https://github.com/python-gitlab/python-gitlab/issues/209) - if isinstance(getattr(self, k, None), BaseManager): - continue - - if isinstance(v, list): - self.__dict__[k] = [] - for i in v: - self.__dict__[k].append(self._get_object(k, i, **kwargs)) - elif v is None: - self.__dict__[k] = None - else: - self.__dict__[k] = self._get_object(k, v, **kwargs) - - def _create(self, **kwargs): - if not self.canCreate: - raise NotImplementedError - - json = self.gitlab.create(self, **kwargs) - self._set_from_dict(json) - self._from_api = True - - def _update(self, **kwargs): - if not self.canUpdate: - raise NotImplementedError - - json = self.gitlab.update(self, **kwargs) - self._set_from_dict(json) - - def save(self, **kwargs): - if self._from_api: - self._update(**kwargs) - else: - self._create(**kwargs) - - def delete(self, **kwargs): - if not self.canDelete: - raise NotImplementedError - - if not self._from_api: - raise GitlabDeleteError("Object not yet created") - - return self.gitlab.delete(self, **kwargs) - - @classmethod - def create(cls, gl, data, **kwargs): - """Create an object. - - Args: - gl (gitlab.Gitlab): Gitlab object referencing the GitLab server. - data (dict): The data used to define the object. - - Returns: - object: The new object. - - Raises: - NotImplementedError: If objects can't be created. - GitlabCreateError: If the server cannot perform the request. - """ - if not cls.canCreate: - raise NotImplementedError - - obj = cls(gl, data, **kwargs) - obj.save() - - return obj - - def __init__(self, gl, data=None, **kwargs): - """Constructs a new object. - - Do not use this method. Use the `get` or `create` class methods - instead. - - Args: - gl (gitlab.Gitlab): Gitlab object referencing the GitLab server. - data: If `data` is a dict, create a new object using the - information. If it is an int or a string, get a GitLab object - from an API request. - **kwargs: Additional arguments to send to GitLab. - """ - self._from_api = False - #: (gitlab.Gitlab): Gitlab connection. - self.gitlab = gl - - if (data is None or isinstance(data, six.integer_types) or - isinstance(data, six.string_types)): - if not self.canGet: - raise NotImplementedError - data = self.gitlab.get(self.__class__, data, **kwargs) - self._from_api = True - - # the API returned a list because custom kwargs where used - # instead of the id to request an object. Usually parameters - # other than an id return ambiguous results. However in the - # gitlab universe iids together with a project_id are - # unambiguous for merge requests and issues, too. - # So if there is only one element we can use it as our data - # source. - if 'iid' in kwargs and isinstance(data, list): - if len(data) < 1: - raise GitlabGetError('Not found') - elif len(data) == 1: - data = data[0] - else: - raise GitlabGetError('Impossible! You found multiple' - ' elements with the same iid.') - - self._set_from_dict(data, **kwargs) - - if kwargs: - for k, v in kwargs.items(): - # Don't overwrite attributes returned by the server (#171) - if k not in self.__dict__ or not self.__dict__[k]: - self.__dict__[k] = v - - # Special handling for api-objects that don't have id-number in api - # responses. Currently only Labels and Files - if not hasattr(self, "id"): - self.id = None - - def _set_manager(self, var, cls, attrs): - manager = cls(self.gitlab, self, attrs) - setattr(self, var, manager) - - def __getattr__(self, name): - # build a manager if it doesn't exist yet - for var, cls, attrs in self.managers: - if var != name: - continue - self._set_manager(var, cls, attrs) - return getattr(self, var) - - raise AttributeError - - def __str__(self): - return '%s => %s' % (type(self), str(self.__dict__)) - - def __repr__(self): - return '<%s %s:%s>' % (self.__class__.__name__, - self.idAttr, - getattr(self, self.idAttr)) - - def display(self, pretty): - if pretty: - self.pretty_print() - else: - self.short_print() - - def short_print(self, depth=0): - """Print the object on the standard output (verbose). - - Args: - depth (int): Used internaly for recursive call. - """ - id = self.__dict__[self.idAttr] - print("%s%s: %s" % (" " * depth * 2, self.idAttr, id)) - if self.shortPrintAttr: - print("%s%s: %s" % (" " * depth * 2, - self.shortPrintAttr.replace('_', '-'), - self.__dict__[self.shortPrintAttr])) - - @staticmethod - def _get_display_encoding(): - return sys.stdout.encoding or sys.getdefaultencoding() - - @staticmethod - def _obj_to_str(obj): - if isinstance(obj, dict): - s = ", ".join(["%s: %s" % - (x, GitlabObject._obj_to_str(y)) - for (x, y) in obj.items()]) - return "{ %s }" % s - elif isinstance(obj, list): - s = ", ".join([GitlabObject._obj_to_str(x) for x in obj]) - return "[ %s ]" % s - elif six.PY2 and isinstance(obj, six.text_type): - return obj.encode(GitlabObject._get_display_encoding(), "replace") - else: - return str(obj) - - def pretty_print(self, depth=0): - """Print the object on the standard output (verbose). - - Args: - depth (int): Used internaly for recursive call. - """ - id = self.__dict__[self.idAttr] - print("%s%s: %s" % (" " * depth * 2, self.idAttr, id)) - for k in sorted(self.__dict__.keys()): - if k in (self.idAttr, 'id', 'gitlab'): - continue - if k[0] == '_': - continue - v = self.__dict__[k] - pretty_k = k.replace('_', '-') - if six.PY2: - pretty_k = pretty_k.encode( - GitlabObject._get_display_encoding(), "replace") - if isinstance(v, GitlabObject): - if depth == 0: - print("%s:" % pretty_k) - v.pretty_print(1) - else: - print("%s: %s" % (pretty_k, v.id)) - elif isinstance(v, BaseManager): - continue - else: - if hasattr(v, __name__) and v.__name__ == 'Gitlab': - continue - v = GitlabObject._obj_to_str(v) - print("%s%s: %s" % (" " * depth * 2, pretty_k, v)) - - def json(self): - """Dump the object as json. - - Returns: - str: The json string. - """ - return json.dumps(self, cls=jsonEncoder) - - def as_dict(self): - """Dump the object as a dict.""" - return {k: v for k, v in six.iteritems(self.__dict__) - if (not isinstance(v, BaseManager) and not k[0] == '_')} - - def __eq__(self, other): - if type(other) is type(self): - return self.as_dict() == other.as_dict() - return False - - def __ne__(self, other): - return not self.__eq__(other) - - class SidekiqManager(object): """Manager for the Sidekiq methods. @@ -627,9 +126,9 @@ class User(GitlabObject): 'admin', 'can_create_group', 'website_url', 'confirm', 'external', 'organization', 'location'] managers = ( - ('emails', UserEmailManager, [('user_id', 'id')]), - ('keys', UserKeyManager, [('user_id', 'id')]), - ('projects', UserProjectManager, [('user_id', 'id')]), + ('emails', 'UserEmailManager', [('user_id', 'id')]), + ('keys', 'UserKeyManager', [('user_id', 'id')]), + ('projects', 'UserProjectManager', [('user_id', 'id')]), ) def _data_for_gitlab(self, extra_parameters={}, update=False, @@ -736,8 +235,8 @@ class CurrentUser(GitlabObject): canDelete = False shortPrintAttr = 'username' managers = ( - ('emails', CurrentUserEmailManager, [('user_id', 'id')]), - ('keys', CurrentUserKeyManager, [('user_id', 'id')]), + ('emails', 'CurrentUserEmailManager', [('user_id', 'id')]), + ('keys', 'CurrentUserKeyManager', [('user_id', 'id')]), ) @@ -1069,7 +568,7 @@ class ProjectBoard(GitlabObject): canCreate = False canDelete = False managers = ( - ('lists', ProjectBoardListManager, + ('lists', 'ProjectBoardListManager', [('project_id', 'project_id'), ('board_id', 'id')]), ) @@ -1246,9 +745,9 @@ class ProjectCommit(GitlabObject): optionalCreateAttrs = ['author_email', 'author_name'] shortPrintAttr = 'title' managers = ( - ('comments', ProjectCommitCommentManager, + ('comments', 'ProjectCommitCommentManager', [('project_id', 'project_id'), ('commit_id', 'id')]), - ('statuses', ProjectCommitStatusManager, + ('statuses', 'ProjectCommitStatusManager', [('project_id', 'project_id'), ('commit_id', 'id')]), ) @@ -1433,7 +932,7 @@ class ProjectIssue(GitlabObject): 'updated_at', 'state_event', 'due_date'] shortPrintAttr = 'title' managers = ( - ('notes', ProjectIssueNoteManager, + ('notes', 'ProjectIssueNoteManager', [('project_id', 'project_id'), ('issue_id', 'id')]), ) @@ -1682,9 +1181,9 @@ class ProjectMergeRequest(GitlabObject): optionalListAttrs = ['iid', 'state', 'order_by', 'sort'] managers = ( - ('notes', ProjectMergeRequestNoteManager, + ('notes', 'ProjectMergeRequestNoteManager', [('project_id', 'project_id'), ('merge_request_id', 'id')]), - ('diffs', ProjectMergeRequestDiffManager, + ('diffs', 'ProjectMergeRequestDiffManager', [('project_id', 'project_id'), ('merge_request_id', 'id')]), ) @@ -2080,7 +1579,7 @@ class ProjectSnippet(GitlabObject): optionalUpdateAttrs = ['title', 'file_name', 'code', 'visibility_level'] shortPrintAttr = 'title' managers = ( - ('notes', ProjectSnippetNoteManager, + ('notes', 'ProjectSnippetNoteManager', [('project_id', 'project_id'), ('snippet_id', 'id')]), ) @@ -2299,35 +1798,36 @@ class Project(GitlabObject): 'lfs_enabled', 'request_access_enabled'] shortPrintAttr = 'path' managers = ( - ('accessrequests', ProjectAccessRequestManager, + ('accessrequests', 'ProjectAccessRequestManager', + [('project_id', 'id')]), + ('boards', 'ProjectBoardManager', [('project_id', 'id')]), + ('board_lists', 'ProjectBoardListManager', [('project_id', 'id')]), + ('branches', 'ProjectBranchManager', [('project_id', 'id')]), + ('builds', 'ProjectBuildManager', [('project_id', 'id')]), + ('commits', 'ProjectCommitManager', [('project_id', 'id')]), + ('deployments', 'ProjectDeploymentManager', [('project_id', 'id')]), + ('environments', 'ProjectEnvironmentManager', [('project_id', 'id')]), + ('events', 'ProjectEventManager', [('project_id', 'id')]), + ('files', 'ProjectFileManager', [('project_id', 'id')]), + ('forks', 'ProjectForkManager', [('project_id', 'id')]), + ('hooks', 'ProjectHookManager', [('project_id', 'id')]), + ('keys', 'ProjectKeyManager', [('project_id', 'id')]), + ('issues', 'ProjectIssueManager', [('project_id', 'id')]), + ('labels', 'ProjectLabelManager', [('project_id', 'id')]), + ('members', 'ProjectMemberManager', [('project_id', 'id')]), + ('mergerequests', 'ProjectMergeRequestManager', [('project_id', 'id')]), - ('boards', ProjectBoardManager, [('project_id', 'id')]), - ('board_lists', ProjectBoardListManager, [('project_id', 'id')]), - ('branches', ProjectBranchManager, [('project_id', 'id')]), - ('builds', ProjectBuildManager, [('project_id', 'id')]), - ('commits', ProjectCommitManager, [('project_id', 'id')]), - ('deployments', ProjectDeploymentManager, [('project_id', 'id')]), - ('environments', ProjectEnvironmentManager, [('project_id', 'id')]), - ('events', ProjectEventManager, [('project_id', 'id')]), - ('files', ProjectFileManager, [('project_id', 'id')]), - ('forks', ProjectForkManager, [('project_id', 'id')]), - ('hooks', ProjectHookManager, [('project_id', 'id')]), - ('keys', ProjectKeyManager, [('project_id', 'id')]), - ('issues', ProjectIssueManager, [('project_id', 'id')]), - ('labels', ProjectLabelManager, [('project_id', 'id')]), - ('members', ProjectMemberManager, [('project_id', 'id')]), - ('mergerequests', ProjectMergeRequestManager, [('project_id', 'id')]), - ('milestones', ProjectMilestoneManager, [('project_id', 'id')]), - ('notes', ProjectNoteManager, [('project_id', 'id')]), - ('notificationsettings', ProjectNotificationSettingsManager, + ('milestones', 'ProjectMilestoneManager', [('project_id', 'id')]), + ('notes', 'ProjectNoteManager', [('project_id', 'id')]), + ('notificationsettings', 'ProjectNotificationSettingsManager', [('project_id', 'id')]), - ('pipelines', ProjectPipelineManager, [('project_id', 'id')]), - ('runners', ProjectRunnerManager, [('project_id', 'id')]), - ('services', ProjectServiceManager, [('project_id', 'id')]), - ('snippets', ProjectSnippetManager, [('project_id', 'id')]), - ('tags', ProjectTagManager, [('project_id', 'id')]), - ('triggers', ProjectTriggerManager, [('project_id', 'id')]), - ('variables', ProjectVariableManager, [('project_id', 'id')]), + ('pipelines', 'ProjectPipelineManager', [('project_id', 'id')]), + ('runners', 'ProjectRunnerManager', [('project_id', 'id')]), + ('services', 'ProjectServiceManager', [('project_id', 'id')]), + ('snippets', 'ProjectSnippetManager', [('project_id', 'id')]), + ('tags', 'ProjectTagManager', [('project_id', 'id')]), + ('triggers', 'ProjectTriggerManager', [('project_id', 'id')]), + ('variables', 'ProjectVariableManager', [('project_id', 'id')]), ) VISIBILITY_PRIVATE = gitlab.VISIBILITY_PRIVATE @@ -2768,12 +2268,12 @@ class Group(GitlabObject): 'lfs_enabled', 'request_access_enabled'] shortPrintAttr = 'name' managers = ( - ('accessrequests', GroupAccessRequestManager, [('group_id', 'id')]), - ('members', GroupMemberManager, [('group_id', 'id')]), - ('notificationsettings', GroupNotificationSettingsManager, + ('accessrequests', 'GroupAccessRequestManager', [('group_id', 'id')]), + ('members', 'GroupMemberManager', [('group_id', 'id')]), + ('notificationsettings', 'GroupNotificationSettingsManager', [('group_id', 'id')]), - ('projects', GroupProjectManager, [('group_id', 'id')]), - ('issues', GroupIssueManager, [('group_id', 'id')]), + ('projects', 'GroupProjectManager', [('group_id', 'id')]), + ('issues', 'GroupIssueManager', [('group_id', 'id')]), ) GUEST_ACCESS = gitlab.GUEST_ACCESS @@ -2842,8 +2342,8 @@ class Team(GitlabObject): requiredCreateAttrs = ['name', 'path'] canUpdate = False managers = ( - ('members', TeamMemberManager, [('team_id', 'id')]), - ('projects', TeamProjectManager, [('team_id', 'id')]), + ('members', 'TeamMemberManager', [('team_id', 'id')]), + ('projects', 'TeamProjectManager', [('team_id', 'id')]), ) diff --git a/gitlab/v4/__init__.py b/gitlab/v4/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py new file mode 100644 index 000000000..5ffd01784 --- /dev/null +++ b/gitlab/v4/objects.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2013-2015 Gauvain Pocentek +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +from gitlab.v3.objects import * # noqa diff --git a/tools/python_test.py b/tools/python_test.py index 41df22150..b56a97db9 100644 --- a/tools/python_test.py +++ b/tools/python_test.py @@ -29,7 +29,7 @@ gl = gitlab.Gitlab.from_config(config_files=['/tmp/python-gitlab.cfg']) assert(token_from_auth == gl.private_token) gl.auth() -assert(isinstance(gl.user, gitlab.objects.CurrentUser)) +assert(isinstance(gl.user, gitlab.v3.objects.CurrentUser)) # settings settings = gl.settings.get() From 3aa6b48f47d6ec2b6153d56b01b4b0151212c7e3 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Tue, 23 May 2017 21:47:30 +0200 Subject: [PATCH 05/32] Duplicate the v3/objects.py in v4/ Using imports from v3/objects.py in v4/objects.py will have side effects. Duplication is not the most elegant choice but v4 is the future and v3 will die eventually. --- gitlab/v4/objects.py | 2337 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 2335 insertions(+), 2 deletions(-) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index 5ffd01784..01bb67040 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2013-2015 Gauvain Pocentek +# Copyright (C) 2013-2017 Gauvain Pocentek # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by @@ -15,4 +15,2337 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . -from gitlab.v3.objects import * # noqa +from __future__ import print_function +from __future__ import division +from __future__ import absolute_import +import base64 +import json +import urllib +import warnings + +import six + +import gitlab +from gitlab.base import * # noqa +from gitlab.exceptions import * # noqa +from gitlab import utils + + +class SidekiqManager(object): + """Manager for the Sidekiq methods. + + This manager doesn't actually manage objects but provides helper fonction + for the sidekiq metrics API. + """ + def __init__(self, gl): + """Constructs a Sidekiq manager. + + Args: + gl (gitlab.Gitlab): Gitlab object referencing the GitLab server. + """ + self.gitlab = gl + + def _simple_get(self, url, **kwargs): + r = self.gitlab._raw_get(url, **kwargs) + raise_error_from_response(r, GitlabGetError) + return r.json() + + def queue_metrics(self, **kwargs): + """Returns the registred queues information.""" + return self._simple_get('/sidekiq/queue_metrics', **kwargs) + + def process_metrics(self, **kwargs): + """Returns the registred sidekiq workers.""" + return self._simple_get('/sidekiq/process_metrics', **kwargs) + + def job_stats(self, **kwargs): + """Returns statistics about the jobs performed.""" + return self._simple_get('/sidekiq/job_stats', **kwargs) + + def compound_metrics(self, **kwargs): + """Returns all available metrics and statistics.""" + return self._simple_get('/sidekiq/compound_metrics', **kwargs) + + +class UserEmail(GitlabObject): + _url = '/users/%(user_id)s/emails' + canUpdate = False + shortPrintAttr = 'email' + requiredUrlAttrs = ['user_id'] + requiredCreateAttrs = ['email'] + + +class UserEmailManager(BaseManager): + obj_cls = UserEmail + + +class UserKey(GitlabObject): + _url = '/users/%(user_id)s/keys' + canGet = 'from_list' + canUpdate = False + requiredUrlAttrs = ['user_id'] + requiredCreateAttrs = ['title', 'key'] + + +class UserKeyManager(BaseManager): + obj_cls = UserKey + + +class UserProject(GitlabObject): + _url = '/projects/user/%(user_id)s' + _constructorTypes = {'owner': 'User', 'namespace': 'Group'} + canUpdate = False + canDelete = False + canList = False + canGet = False + requiredUrlAttrs = ['user_id'] + requiredCreateAttrs = ['name'] + optionalCreateAttrs = ['default_branch', 'issues_enabled', 'wall_enabled', + 'merge_requests_enabled', 'wiki_enabled', + 'snippets_enabled', 'public', 'visibility_level', + 'description', 'builds_enabled', 'public_builds', + 'import_url', 'only_allow_merge_if_build_succeeds'] + + +class UserProjectManager(BaseManager): + obj_cls = UserProject + + +class User(GitlabObject): + _url = '/users' + shortPrintAttr = 'username' + requiredCreateAttrs = ['email', 'username', 'name'] + optionalCreateAttrs = ['password', 'reset_password', 'skype', 'linkedin', + 'twitter', 'projects_limit', 'extern_uid', + 'provider', 'bio', 'admin', 'can_create_group', + 'website_url', 'confirm', 'external', + 'organization', 'location'] + requiredUpdateAttrs = ['email', 'username', 'name'] + optionalUpdateAttrs = ['password', 'skype', 'linkedin', 'twitter', + 'projects_limit', 'extern_uid', 'provider', 'bio', + 'admin', 'can_create_group', 'website_url', + 'confirm', 'external', 'organization', 'location'] + managers = ( + ('emails', 'UserEmailManager', [('user_id', 'id')]), + ('keys', 'UserKeyManager', [('user_id', 'id')]), + ('projects', 'UserProjectManager', [('user_id', 'id')]), + ) + + def _data_for_gitlab(self, extra_parameters={}, update=False, + as_json=True): + if hasattr(self, 'confirm'): + self.confirm = str(self.confirm).lower() + return super(User, self)._data_for_gitlab(extra_parameters) + + def block(self, **kwargs): + """Blocks the user.""" + url = '/users/%s/block' % self.id + r = self.gitlab._raw_put(url, **kwargs) + raise_error_from_response(r, GitlabBlockError) + self.state = 'blocked' + + def unblock(self, **kwargs): + """Unblocks the user.""" + url = '/users/%s/unblock' % self.id + r = self.gitlab._raw_put(url, **kwargs) + raise_error_from_response(r, GitlabUnblockError) + self.state = 'active' + + def __eq__(self, other): + if type(other) is type(self): + selfdict = self.as_dict() + otherdict = other.as_dict() + selfdict.pop('password', None) + otherdict.pop('password', None) + return selfdict == otherdict + return False + + +class UserManager(BaseManager): + obj_cls = User + + def search(self, query, **kwargs): + """Search users. + + Args: + query (str): The query string to send to GitLab for the search. + all (bool): If True, return all the items, without pagination + **kwargs: Additional arguments to send to GitLab. + + Returns: + list(User): A list of matching users. + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabListError: If the server fails to perform the request. + """ + url = self.obj_cls._url + '?search=' + query + return self.gitlab._raw_list(url, self.obj_cls, **kwargs) + + def get_by_username(self, username, **kwargs): + """Get a user by its username. + + Args: + username (str): The name of the user. + **kwargs: Additional arguments to send to GitLab. + + Returns: + User: The matching user. + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabGetError: If the server fails to perform the request. + """ + url = self.obj_cls._url + '?username=' + username + results = self.gitlab._raw_list(url, self.obj_cls, **kwargs) + assert len(results) in (0, 1) + try: + return results[0] + except IndexError: + raise GitlabGetError('no such user: ' + username) + + +class CurrentUserEmail(GitlabObject): + _url = '/user/emails' + canUpdate = False + shortPrintAttr = 'email' + requiredCreateAttrs = ['email'] + + +class CurrentUserEmailManager(BaseManager): + obj_cls = CurrentUserEmail + + +class CurrentUserKey(GitlabObject): + _url = '/user/keys' + canUpdate = False + shortPrintAttr = 'title' + requiredCreateAttrs = ['title', 'key'] + + +class CurrentUserKeyManager(BaseManager): + obj_cls = CurrentUserKey + + +class CurrentUser(GitlabObject): + _url = '/user' + canList = False + canCreate = False + canUpdate = False + canDelete = False + shortPrintAttr = 'username' + managers = ( + ('emails', 'CurrentUserEmailManager', [('user_id', 'id')]), + ('keys', 'CurrentUserKeyManager', [('user_id', 'id')]), + ) + + +class ApplicationSettings(GitlabObject): + _url = '/application/settings' + _id_in_update_url = False + getRequiresId = False + optionalUpdateAttrs = ['after_sign_out_path', + 'container_registry_token_expire_delay', + 'default_branch_protection', + 'default_project_visibility', + 'default_projects_limit', + 'default_snippet_visibility', + 'domain_blacklist', + 'domain_blacklist_enabled', + 'domain_whitelist', + 'enabled_git_access_protocol', + 'gravatar_enabled', + 'home_page_url', + 'max_attachment_size', + 'repository_storage', + 'restricted_signup_domains', + 'restricted_visibility_levels', + 'session_expire_delay', + 'sign_in_text', + 'signin_enabled', + 'signup_enabled', + 'twitter_sharing_enabled', + 'user_oauth_applications'] + canList = False + canCreate = False + canDelete = False + + def _data_for_gitlab(self, extra_parameters={}, update=False, + as_json=True): + data = (super(ApplicationSettings, self) + ._data_for_gitlab(extra_parameters, update=update, + as_json=False)) + if not self.domain_whitelist: + data.pop('domain_whitelist', None) + return json.dumps(data) + + +class ApplicationSettingsManager(BaseManager): + obj_cls = ApplicationSettings + + +class BroadcastMessage(GitlabObject): + _url = '/broadcast_messages' + requiredCreateAttrs = ['message'] + optionalCreateAttrs = ['starts_at', 'ends_at', 'color', 'font'] + requiredUpdateAttrs = [] + optionalUpdateAttrs = ['message', 'starts_at', 'ends_at', 'color', 'font'] + + +class BroadcastMessageManager(BaseManager): + obj_cls = BroadcastMessage + + +class Key(GitlabObject): + _url = '/deploy_keys' + canGet = 'from_list' + canCreate = False + canUpdate = False + canDelete = False + + def __init__(self, *args, **kwargs): + warnings.warn("`Key` is deprecated, use `DeployKey` instead", + DeprecationWarning) + super(Key, self).__init__(*args, **kwargs) + + +class KeyManager(BaseManager): + obj_cls = Key + + +class DeployKey(GitlabObject): + _url = '/deploy_keys' + canGet = 'from_list' + canCreate = False + canUpdate = False + canDelete = False + + +class DeployKeyManager(BaseManager): + obj_cls = DeployKey + + +class NotificationSettings(GitlabObject): + _url = '/notification_settings' + _id_in_update_url = False + getRequiresId = False + optionalUpdateAttrs = ['level', + 'notification_email', + 'new_note', + 'new_issue', + 'reopen_issue', + 'close_issue', + 'reassign_issue', + 'new_merge_request', + 'reopen_merge_request', + 'close_merge_request', + 'reassign_merge_request', + 'merge_merge_request'] + canList = False + canCreate = False + canDelete = False + + +class NotificationSettingsManager(BaseManager): + obj_cls = NotificationSettings + + +class Gitignore(GitlabObject): + _url = '/templates/gitignores' + canDelete = False + canUpdate = False + canCreate = False + idAttr = 'name' + + +class GitignoreManager(BaseManager): + obj_cls = Gitignore + + +class Gitlabciyml(GitlabObject): + _url = '/templates/gitlab_ci_ymls' + canDelete = False + canUpdate = False + canCreate = False + idAttr = 'name' + + +class GitlabciymlManager(BaseManager): + obj_cls = Gitlabciyml + + +class GroupIssue(GitlabObject): + _url = '/groups/%(group_id)s/issues' + canGet = 'from_list' + canCreate = False + canUpdate = False + canDelete = False + requiredUrlAttrs = ['group_id'] + optionalListAttrs = ['state', 'labels', 'milestone', 'order_by', 'sort'] + + +class GroupIssueManager(BaseManager): + obj_cls = GroupIssue + + +class GroupMember(GitlabObject): + _url = '/groups/%(group_id)s/members' + canGet = 'from_list' + requiredUrlAttrs = ['group_id'] + requiredCreateAttrs = ['access_level', 'user_id'] + optionalCreateAttrs = ['expires_at'] + requiredUpdateAttrs = ['access_level'] + optionalCreateAttrs = ['expires_at'] + shortPrintAttr = 'username' + + def _update(self, **kwargs): + self.user_id = self.id + super(GroupMember, self)._update(**kwargs) + + +class GroupMemberManager(BaseManager): + obj_cls = GroupMember + + +class GroupNotificationSettings(NotificationSettings): + _url = '/groups/%(group_id)s/notification_settings' + requiredUrlAttrs = ['group_id'] + + +class GroupNotificationSettingsManager(BaseManager): + obj_cls = GroupNotificationSettings + + +class GroupAccessRequest(GitlabObject): + _url = '/groups/%(group_id)s/access_requests' + canGet = 'from_list' + canUpdate = False + + def approve(self, access_level=gitlab.DEVELOPER_ACCESS, **kwargs): + """Approve an access request. + + Attrs: + access_level (int): The access level for the user. + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabUpdateError: If the server fails to perform the request. + """ + + url = ('/groups/%(group_id)s/access_requests/%(id)s/approve' % + {'group_id': self.group_id, 'id': self.id}) + data = {'access_level': access_level} + r = self.gitlab._raw_put(url, data=data, **kwargs) + raise_error_from_response(r, GitlabUpdateError, 201) + self._set_from_dict(r.json()) + + +class GroupAccessRequestManager(BaseManager): + obj_cls = GroupAccessRequest + + +class Hook(GitlabObject): + _url = '/hooks' + canUpdate = False + requiredCreateAttrs = ['url'] + shortPrintAttr = 'url' + + +class HookManager(BaseManager): + obj_cls = Hook + + +class Issue(GitlabObject): + _url = '/issues' + _constructorTypes = {'author': 'User', 'assignee': 'User', + 'milestone': 'ProjectMilestone'} + canGet = 'from_list' + canDelete = False + canUpdate = False + canCreate = False + shortPrintAttr = 'title' + optionalListAttrs = ['state', 'labels', 'order_by', 'sort'] + + +class IssueManager(BaseManager): + obj_cls = Issue + + +class License(GitlabObject): + _url = '/licenses' + canDelete = False + canUpdate = False + canCreate = False + idAttr = 'key' + + optionalListAttrs = ['popular'] + optionalGetAttrs = ['project', 'fullname'] + + +class LicenseManager(BaseManager): + obj_cls = License + + +class Snippet(GitlabObject): + _url = '/snippets' + _constructorTypes = {'author': 'User'} + requiredCreateAttrs = ['title', 'file_name', 'content'] + optionalCreateAttrs = ['lifetime', 'visibility_level'] + optionalUpdateAttrs = ['title', 'file_name', 'content', 'visibility_level'] + shortPrintAttr = 'title' + + def raw(self, streamed=False, action=None, chunk_size=1024, **kwargs): + """Return the raw content of a snippet. + + Args: + streamed (bool): If True the data will be processed by chunks of + `chunk_size` and each chunk is passed to `action` for + treatment. + action (callable): Callable responsible of dealing with chunk of + data. + chunk_size (int): Size of each chunk. + + Returns: + str: The snippet content. + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabGetError: If the server fails to perform the request. + """ + url = ("/snippets/%(snippet_id)s/raw" % {'snippet_id': self.id}) + r = self.gitlab._raw_get(url, **kwargs) + raise_error_from_response(r, GitlabGetError) + return utils.response_content(r, streamed, action, chunk_size) + + +class SnippetManager(BaseManager): + obj_cls = Snippet + + def public(self, **kwargs): + """List all the public snippets. + + Args: + all (bool): If True, return all the items, without pagination + **kwargs: Additional arguments to send to GitLab. + + Returns: + list(gitlab.Gitlab.Snippet): The list of snippets. + """ + return self.gitlab._raw_list("/snippets/public", Snippet, **kwargs) + + +class Namespace(GitlabObject): + _url = '/namespaces' + canGet = 'from_list' + canUpdate = False + canDelete = False + canCreate = False + optionalListAttrs = ['search'] + + +class NamespaceManager(BaseManager): + obj_cls = Namespace + + +class ProjectBoardList(GitlabObject): + _url = '/projects/%(project_id)s/boards/%(board_id)s/lists' + requiredUrlAttrs = ['project_id', 'board_id'] + _constructorTypes = {'label': 'ProjectLabel'} + requiredCreateAttrs = ['label_id'] + requiredUpdateAttrs = ['position'] + + +class ProjectBoardListManager(BaseManager): + obj_cls = ProjectBoardList + + +class ProjectBoard(GitlabObject): + _url = '/projects/%(project_id)s/boards' + requiredUrlAttrs = ['project_id'] + _constructorTypes = {'labels': 'ProjectBoardList'} + canGet = 'from_list' + canUpdate = False + canCreate = False + canDelete = False + managers = ( + ('lists', 'ProjectBoardListManager', + [('project_id', 'project_id'), ('board_id', 'id')]), + ) + + +class ProjectBoardManager(BaseManager): + obj_cls = ProjectBoard + + +class ProjectBranch(GitlabObject): + _url = '/projects/%(project_id)s/repository/branches' + _constructorTypes = {'author': 'User', "committer": "User"} + + idAttr = 'name' + canUpdate = False + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['branch_name', 'ref'] + + def protect(self, protect=True, **kwargs): + """Protects the branch.""" + url = self._url % {'project_id': self.project_id} + action = 'protect' if protect else 'unprotect' + url = "%s/%s/%s" % (url, self.name, action) + r = self.gitlab._raw_put(url, data=None, content_type=None, **kwargs) + raise_error_from_response(r, GitlabProtectError) + + if protect: + self.protected = protect + else: + del self.protected + + def unprotect(self, **kwargs): + """Unprotects the branch.""" + self.protect(False, **kwargs) + + +class ProjectBranchManager(BaseManager): + obj_cls = ProjectBranch + + +class ProjectBuild(GitlabObject): + _url = '/projects/%(project_id)s/builds' + _constructorTypes = {'user': 'User', + 'commit': 'ProjectCommit', + 'runner': 'Runner'} + requiredUrlAttrs = ['project_id'] + canDelete = False + canUpdate = False + canCreate = False + + def cancel(self, **kwargs): + """Cancel the build.""" + url = '/projects/%s/builds/%s/cancel' % (self.project_id, self.id) + r = self.gitlab._raw_post(url) + raise_error_from_response(r, GitlabBuildCancelError, 201) + + def retry(self, **kwargs): + """Retry the build.""" + url = '/projects/%s/builds/%s/retry' % (self.project_id, self.id) + r = self.gitlab._raw_post(url) + raise_error_from_response(r, GitlabBuildRetryError, 201) + + def play(self, **kwargs): + """Trigger a build explicitly.""" + url = '/projects/%s/builds/%s/play' % (self.project_id, self.id) + r = self.gitlab._raw_post(url) + raise_error_from_response(r, GitlabBuildPlayError) + + def erase(self, **kwargs): + """Erase the build (remove build artifacts and trace).""" + url = '/projects/%s/builds/%s/erase' % (self.project_id, self.id) + r = self.gitlab._raw_post(url) + raise_error_from_response(r, GitlabBuildEraseError, 201) + + def keep_artifacts(self, **kwargs): + """Prevent artifacts from being delete when expiration is set. + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabCreateError: If the request failed. + """ + url = ('/projects/%s/builds/%s/artifacts/keep' % + (self.project_id, self.id)) + r = self.gitlab._raw_post(url) + raise_error_from_response(r, GitlabGetError, 200) + + def artifacts(self, streamed=False, action=None, chunk_size=1024, + **kwargs): + """Get the build artifacts. + + Args: + streamed (bool): If True the data will be processed by chunks of + `chunk_size` and each chunk is passed to `action` for + treatment. + action (callable): Callable responsible of dealing with chunk of + data. + chunk_size (int): Size of each chunk. + + Returns: + str: The artifacts if `streamed` is False, None otherwise. + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabGetError: If the artifacts are not available. + """ + url = '/projects/%s/builds/%s/artifacts' % (self.project_id, self.id) + r = self.gitlab._raw_get(url, streamed=streamed, **kwargs) + raise_error_from_response(r, GitlabGetError, 200) + return utils.response_content(r, streamed, action, chunk_size) + + def trace(self, streamed=False, action=None, chunk_size=1024, **kwargs): + """Get the build trace. + + Args: + streamed (bool): If True the data will be processed by chunks of + `chunk_size` and each chunk is passed to `action` for + treatment. + action (callable): Callable responsible of dealing with chunk of + data. + chunk_size (int): Size of each chunk. + + Returns: + str: The trace. + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabGetError: If the trace is not available. + """ + url = '/projects/%s/builds/%s/trace' % (self.project_id, self.id) + r = self.gitlab._raw_get(url, streamed=streamed, **kwargs) + raise_error_from_response(r, GitlabGetError, 200) + return utils.response_content(r, streamed, action, chunk_size) + + +class ProjectBuildManager(BaseManager): + obj_cls = ProjectBuild + + +class ProjectCommitStatus(GitlabObject): + _url = '/projects/%(project_id)s/repository/commits/%(commit_id)s/statuses' + _create_url = '/projects/%(project_id)s/statuses/%(commit_id)s' + canUpdate = False + canDelete = False + requiredUrlAttrs = ['project_id', 'commit_id'] + optionalGetAttrs = ['ref_name', 'stage', 'name', 'all'] + requiredCreateAttrs = ['state'] + optionalCreateAttrs = ['description', 'name', 'context', 'ref', + 'target_url'] + + +class ProjectCommitStatusManager(BaseManager): + obj_cls = ProjectCommitStatus + + +class ProjectCommitComment(GitlabObject): + _url = '/projects/%(project_id)s/repository/commits/%(commit_id)s/comments' + canUpdate = False + canGet = False + canDelete = False + requiredUrlAttrs = ['project_id', 'commit_id'] + requiredCreateAttrs = ['note'] + optionalCreateAttrs = ['path', 'line', 'line_type'] + + +class ProjectCommitCommentManager(BaseManager): + obj_cls = ProjectCommitComment + + +class ProjectCommit(GitlabObject): + _url = '/projects/%(project_id)s/repository/commits' + canDelete = False + canUpdate = False + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['branch_name', 'commit_message', 'actions'] + optionalCreateAttrs = ['author_email', 'author_name'] + shortPrintAttr = 'title' + managers = ( + ('comments', 'ProjectCommitCommentManager', + [('project_id', 'project_id'), ('commit_id', 'id')]), + ('statuses', 'ProjectCommitStatusManager', + [('project_id', 'project_id'), ('commit_id', 'id')]), + ) + + def diff(self, **kwargs): + """Generate the commit diff.""" + url = ('/projects/%(project_id)s/repository/commits/%(commit_id)s/diff' + % {'project_id': self.project_id, 'commit_id': self.id}) + r = self.gitlab._raw_get(url, **kwargs) + raise_error_from_response(r, GitlabGetError) + + return r.json() + + def blob(self, filepath, streamed=False, action=None, chunk_size=1024, + **kwargs): + """Generate the content of a file for this commit. + + Args: + filepath (str): Path of the file to request. + streamed (bool): If True the data will be processed by chunks of + `chunk_size` and each chunk is passed to `action` for + treatment. + action (callable): Callable responsible of dealing with chunk of + data. + chunk_size (int): Size of each chunk. + + Returns: + str: The content of the file + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabGetError: If the server fails to perform the request. + """ + url = ('/projects/%(project_id)s/repository/blobs/%(commit_id)s' % + {'project_id': self.project_id, 'commit_id': self.id}) + url += '?filepath=%s' % filepath + r = self.gitlab._raw_get(url, streamed=streamed, **kwargs) + raise_error_from_response(r, GitlabGetError) + return utils.response_content(r, streamed, action, chunk_size) + + def builds(self, **kwargs): + """List the build for this commit. + + Returns: + list(ProjectBuild): A list of builds. + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabListError: If the server fails to perform the request. + """ + url = '/projects/%s/repository/commits/%s/builds' % (self.project_id, + self.id) + return self.gitlab._raw_list(url, ProjectBuild, + {'project_id': self.project_id}, + **kwargs) + + def cherry_pick(self, branch, **kwargs): + """Cherry-pick a commit into a branch. + + Args: + branch (str): Name of target branch. + + Raises: + GitlabCherryPickError: If the cherry pick could not be applied. + """ + url = ('/projects/%s/repository/commits/%s/cherry_pick' % + (self.project_id, self.id)) + + r = self.gitlab._raw_post(url, data={'project_id': self.project_id, + 'branch': branch}, **kwargs) + errors = {400: GitlabCherryPickError} + raise_error_from_response(r, errors, expected_code=201) + + +class ProjectCommitManager(BaseManager): + obj_cls = ProjectCommit + + +class ProjectEnvironment(GitlabObject): + _url = '/projects/%(project_id)s/environments' + canGet = 'from_list' + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['name'] + optionalCreateAttrs = ['external_url'] + optionalUpdateAttrs = ['name', 'external_url'] + + +class ProjectEnvironmentManager(BaseManager): + obj_cls = ProjectEnvironment + + +class ProjectKey(GitlabObject): + _url = '/projects/%(project_id)s/keys' + canUpdate = False + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['title', 'key'] + + +class ProjectKeyManager(BaseManager): + obj_cls = ProjectKey + + def enable(self, key_id): + """Enable a deploy key for a project.""" + url = '/projects/%s/deploy_keys/%s/enable' % (self.parent.id, key_id) + r = self.gitlab._raw_post(url) + raise_error_from_response(r, GitlabProjectDeployKeyError, 201) + + def disable(self, key_id): + """Disable a deploy key for a project.""" + url = '/projects/%s/deploy_keys/%s/disable' % (self.parent.id, key_id) + r = self.gitlab._raw_delete(url) + raise_error_from_response(r, GitlabProjectDeployKeyError, 200) + + +class ProjectEvent(GitlabObject): + _url = '/projects/%(project_id)s/events' + canGet = 'from_list' + canDelete = False + canUpdate = False + canCreate = False + requiredUrlAttrs = ['project_id'] + shortPrintAttr = 'target_title' + + +class ProjectEventManager(BaseManager): + obj_cls = ProjectEvent + + +class ProjectFork(GitlabObject): + _url = '/projects/fork/%(project_id)s' + canUpdate = False + canDelete = False + canList = False + canGet = False + requiredUrlAttrs = ['project_id'] + optionalCreateAttrs = ['namespace'] + + +class ProjectForkManager(BaseManager): + obj_cls = ProjectFork + + +class ProjectHook(GitlabObject): + _url = '/projects/%(project_id)s/hooks' + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['url'] + optionalCreateAttrs = ['push_events', 'issues_events', 'note_events', + 'merge_requests_events', 'tag_push_events', + 'build_events', 'enable_ssl_verification', 'token', + 'pipeline_events'] + shortPrintAttr = 'url' + + +class ProjectHookManager(BaseManager): + obj_cls = ProjectHook + + +class ProjectIssueNote(GitlabObject): + _url = '/projects/%(project_id)s/issues/%(issue_id)s/notes' + _constructorTypes = {'author': 'User'} + canDelete = False + requiredUrlAttrs = ['project_id', 'issue_id'] + requiredCreateAttrs = ['body'] + optionalCreateAttrs = ['created_at'] + + +class ProjectIssueNoteManager(BaseManager): + obj_cls = ProjectIssueNote + + +class ProjectIssue(GitlabObject): + _url = '/projects/%(project_id)s/issues/' + _constructorTypes = {'author': 'User', 'assignee': 'User', + 'milestone': 'ProjectMilestone'} + optionalListAttrs = ['state', 'labels', 'milestone', 'iid', 'order_by', + 'sort'] + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['title'] + optionalCreateAttrs = ['description', 'assignee_id', 'milestone_id', + 'labels', 'created_at', 'due_date'] + optionalUpdateAttrs = ['title', 'description', 'assignee_id', + 'milestone_id', 'labels', 'created_at', + 'updated_at', 'state_event', 'due_date'] + shortPrintAttr = 'title' + managers = ( + ('notes', 'ProjectIssueNoteManager', + [('project_id', 'project_id'), ('issue_id', 'id')]), + ) + + def subscribe(self, **kwargs): + """Subscribe to an issue. + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabSubscribeError: If the subscription cannot be done + """ + url = ('/projects/%(project_id)s/issues/%(issue_id)s/subscription' % + {'project_id': self.project_id, 'issue_id': self.id}) + + r = self.gitlab._raw_post(url, **kwargs) + raise_error_from_response(r, GitlabSubscribeError) + self._set_from_dict(r.json()) + + def unsubscribe(self, **kwargs): + """Unsubscribe an issue. + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabUnsubscribeError: If the unsubscription cannot be done + """ + url = ('/projects/%(project_id)s/issues/%(issue_id)s/subscription' % + {'project_id': self.project_id, 'issue_id': self.id}) + + r = self.gitlab._raw_delete(url, **kwargs) + raise_error_from_response(r, GitlabUnsubscribeError) + self._set_from_dict(r.json()) + + def move(self, to_project_id, **kwargs): + """Move the issue to another project. + + Raises: + GitlabConnectionError: If the server cannot be reached. + """ + url = ('/projects/%(project_id)s/issues/%(issue_id)s/move' % + {'project_id': self.project_id, 'issue_id': self.id}) + + data = {'to_project_id': to_project_id} + data.update(**kwargs) + r = self.gitlab._raw_post(url, data=data) + raise_error_from_response(r, GitlabUpdateError, 201) + self._set_from_dict(r.json()) + + def todo(self, **kwargs): + """Create a todo for the issue. + + Raises: + GitlabConnectionError: If the server cannot be reached. + """ + url = ('/projects/%(project_id)s/issues/%(issue_id)s/todo' % + {'project_id': self.project_id, 'issue_id': self.id}) + r = self.gitlab._raw_post(url, **kwargs) + raise_error_from_response(r, GitlabTodoError, [201, 304]) + + def time_stats(self, **kwargs): + """Get time stats for the issue. + + Raises: + GitlabConnectionError: If the server cannot be reached. + """ + url = ('/projects/%(project_id)s/issues/%(issue_id)s/time_stats' % + {'project_id': self.project_id, 'issue_id': self.id}) + r = self.gitlab._raw_get(url, **kwargs) + raise_error_from_response(r, GitlabGetError) + return r.json() + + def time_estimate(self, **kwargs): + """Set an estimated time of work for the issue. + + Raises: + GitlabConnectionError: If the server cannot be reached. + """ + url = ('/projects/%(project_id)s/issues/%(issue_id)s/time_estimate' % + {'project_id': self.project_id, 'issue_id': self.id}) + r = self.gitlab._raw_post(url, **kwargs) + raise_error_from_response(r, GitlabTimeTrackingError, 201) + return r.json() + + def reset_time_estimate(self, **kwargs): + """Resets estimated time for the issue to 0 seconds. + + Raises: + GitlabConnectionError: If the server cannot be reached. + """ + url = ('/projects/%(project_id)s/issues/%(issue_id)s/' + 'reset_time_estimate' % + {'project_id': self.project_id, 'issue_id': self.id}) + r = self.gitlab._raw_post(url, **kwargs) + raise_error_from_response(r, GitlabTimeTrackingError, 200) + return r.json() + + def add_spent_time(self, **kwargs): + """Set an estimated time of work for the issue. + + Raises: + GitlabConnectionError: If the server cannot be reached. + """ + url = ('/projects/%(project_id)s/issues/%(issue_id)s/' + 'add_spent_time' % + {'project_id': self.project_id, 'issue_id': self.id}) + r = self.gitlab._raw_post(url, **kwargs) + raise_error_from_response(r, GitlabTimeTrackingError, 200) + return r.json() + + def reset_spent_time(self, **kwargs): + """Set an estimated time of work for the issue. + + Raises: + GitlabConnectionError: If the server cannot be reached. + """ + url = ('/projects/%(project_id)s/issues/%(issue_id)s/' + 'reset_spent_time' % + {'project_id': self.project_id, 'issue_id': self.id}) + r = self.gitlab._raw_post(url, **kwargs) + raise_error_from_response(r, GitlabTimeTrackingError, 200) + return r.json() + + +class ProjectIssueManager(BaseManager): + obj_cls = ProjectIssue + + +class ProjectMember(GitlabObject): + _url = '/projects/%(project_id)s/members' + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['access_level', 'user_id'] + optionalCreateAttrs = ['expires_at'] + requiredUpdateAttrs = ['access_level'] + optionalCreateAttrs = ['expires_at'] + shortPrintAttr = 'username' + + +class ProjectMemberManager(BaseManager): + obj_cls = ProjectMember + + +class ProjectNote(GitlabObject): + _url = '/projects/%(project_id)s/notes' + _constructorTypes = {'author': 'User'} + canUpdate = False + canDelete = False + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['body'] + + +class ProjectNoteManager(BaseManager): + obj_cls = ProjectNote + + +class ProjectNotificationSettings(NotificationSettings): + _url = '/projects/%(project_id)s/notification_settings' + requiredUrlAttrs = ['project_id'] + + +class ProjectNotificationSettingsManager(BaseManager): + obj_cls = ProjectNotificationSettings + + +class ProjectTagRelease(GitlabObject): + _url = '/projects/%(project_id)s/repository/tags/%(tag_name)/release' + canDelete = False + canList = False + requiredUrlAttrs = ['project_id', 'tag_name'] + requiredCreateAttrs = ['description'] + shortPrintAttr = 'description' + + +class ProjectTag(GitlabObject): + _url = '/projects/%(project_id)s/repository/tags' + _constructorTypes = {'release': 'ProjectTagRelease', + 'commit': 'ProjectCommit'} + idAttr = 'name' + canGet = 'from_list' + canUpdate = False + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['tag_name', 'ref'] + optionalCreateAttrs = ['message'] + shortPrintAttr = 'name' + + def set_release_description(self, description): + """Set the release notes on the tag. + + If the release doesn't exist yet, it will be created. If it already + exists, its description will be updated. + + Args: + description (str): Description of the release. + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabCreateError: If the server fails to create the release. + GitlabUpdateError: If the server fails to update the release. + """ + url = '/projects/%s/repository/tags/%s/release' % (self.project_id, + self.name) + if self.release is None: + r = self.gitlab._raw_post(url, data={'description': description}) + raise_error_from_response(r, GitlabCreateError, 201) + else: + r = self.gitlab._raw_put(url, data={'description': description}) + raise_error_from_response(r, GitlabUpdateError, 200) + self.release = ProjectTagRelease(self, r.json()) + + +class ProjectTagManager(BaseManager): + obj_cls = ProjectTag + + +class ProjectMergeRequestDiff(GitlabObject): + _url = ('/projects/%(project_id)s/merge_requests/' + '%(merge_request_id)s/versions') + canCreate = False + canUpdate = False + canDelete = False + requiredUrlAttrs = ['project_id', 'merge_request_id'] + + +class ProjectMergeRequestDiffManager(BaseManager): + obj_cls = ProjectMergeRequestDiff + + +class ProjectMergeRequestNote(GitlabObject): + _url = '/projects/%(project_id)s/merge_requests/%(merge_request_id)s/notes' + _constructorTypes = {'author': 'User'} + requiredUrlAttrs = ['project_id', 'merge_request_id'] + requiredCreateAttrs = ['body'] + + +class ProjectMergeRequestNoteManager(BaseManager): + obj_cls = ProjectMergeRequestNote + + +class ProjectMergeRequest(GitlabObject): + _url = '/projects/%(project_id)s/merge_requests' + _constructorTypes = {'author': 'User', 'assignee': 'User'} + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['source_branch', 'target_branch', 'title'] + optionalCreateAttrs = ['assignee_id', 'description', 'target_project_id', + 'labels', 'milestone_id', 'remove_source_branch'] + optionalUpdateAttrs = ['target_branch', 'assignee_id', 'title', + 'description', 'state_event', 'labels', + 'milestone_id'] + optionalListAttrs = ['iid', 'state', 'order_by', 'sort'] + + managers = ( + ('notes', 'ProjectMergeRequestNoteManager', + [('project_id', 'project_id'), ('merge_request_id', 'id')]), + ('diffs', 'ProjectMergeRequestDiffManager', + [('project_id', 'project_id'), ('merge_request_id', 'id')]), + ) + + def _data_for_gitlab(self, extra_parameters={}, update=False, + as_json=True): + data = (super(ProjectMergeRequest, self) + ._data_for_gitlab(extra_parameters, update=update, + as_json=False)) + if update: + # Drop source_branch attribute as it is not accepted by the gitlab + # server (Issue #76) + data.pop('source_branch', None) + return json.dumps(data) + + def subscribe(self, **kwargs): + """Subscribe to a MR. + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabSubscribeError: If the subscription cannot be done + """ + url = ('/projects/%(project_id)s/merge_requests/%(mr_id)s/' + 'subscription' % + {'project_id': self.project_id, 'mr_id': self.id}) + + r = self.gitlab._raw_post(url, **kwargs) + raise_error_from_response(r, GitlabSubscribeError, [201, 304]) + if r.status_code == 201: + self._set_from_dict(r.json()) + + def unsubscribe(self, **kwargs): + """Unsubscribe a MR. + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabUnsubscribeError: If the unsubscription cannot be done + """ + url = ('/projects/%(project_id)s/merge_requests/%(mr_id)s/' + 'subscription' % + {'project_id': self.project_id, 'mr_id': self.id}) + + r = self.gitlab._raw_delete(url, **kwargs) + raise_error_from_response(r, GitlabUnsubscribeError, [200, 304]) + if r.status_code == 200: + self._set_from_dict(r.json()) + + def cancel_merge_when_build_succeeds(self, **kwargs): + """Cancel merge when build succeeds.""" + + u = ('/projects/%s/merge_requests/%s/cancel_merge_when_build_succeeds' + % (self.project_id, self.id)) + r = self.gitlab._raw_put(u, **kwargs) + errors = {401: GitlabMRForbiddenError, + 405: GitlabMRClosedError, + 406: GitlabMROnBuildSuccessError} + raise_error_from_response(r, errors) + return ProjectMergeRequest(self, r.json()) + + def closes_issues(self, **kwargs): + """List issues closed by the MR. + + Returns: + list (ProjectIssue): List of closed issues + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabGetError: If the server fails to perform the request. + """ + url = ('/projects/%s/merge_requests/%s/closes_issues' % + (self.project_id, self.id)) + return self.gitlab._raw_list(url, ProjectIssue, + {'project_id': self.project_id}, + **kwargs) + + def commits(self, **kwargs): + """List the merge request commits. + + Returns: + list (ProjectCommit): List of commits + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabListError: If the server fails to perform the request. + """ + url = ('/projects/%s/merge_requests/%s/commits' % + (self.project_id, self.id)) + return self.gitlab._raw_list(url, ProjectCommit, + {'project_id': self.project_id}, + **kwargs) + + def changes(self, **kwargs): + """List the merge request changes. + + Returns: + list (dict): List of changes + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabListError: If the server fails to perform the request. + """ + url = ('/projects/%s/merge_requests/%s/changes' % + (self.project_id, self.id)) + r = self.gitlab._raw_get(url, **kwargs) + raise_error_from_response(r, GitlabListError) + return r.json() + + def merge(self, merge_commit_message=None, + should_remove_source_branch=False, + merged_when_build_succeeds=False, + **kwargs): + """Accept the merge request. + + Args: + merge_commit_message (bool): Commit message + should_remove_source_branch (bool): If True, removes the source + branch + merged_when_build_succeeds (bool): Wait for the build to succeed, + then merge + + Returns: + ProjectMergeRequest: The updated MR + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabMRForbiddenError: If the user doesn't have permission to + close thr MR + GitlabMRClosedError: If the MR is already closed + """ + url = '/projects/%s/merge_requests/%s/merge' % (self.project_id, + self.id) + data = {} + if merge_commit_message: + data['merge_commit_message'] = merge_commit_message + if should_remove_source_branch: + data['should_remove_source_branch'] = True + if merged_when_build_succeeds: + data['merged_when_build_succeeds'] = True + + r = self.gitlab._raw_put(url, data=data, **kwargs) + errors = {401: GitlabMRForbiddenError, + 405: GitlabMRClosedError} + raise_error_from_response(r, errors) + self._set_from_dict(r.json()) + + def todo(self, **kwargs): + """Create a todo for the merge request. + + Raises: + GitlabConnectionError: If the server cannot be reached. + """ + url = ('/projects/%(project_id)s/merge_requests/%(mr_id)s/todo' % + {'project_id': self.project_id, 'mr_id': self.id}) + r = self.gitlab._raw_post(url, **kwargs) + raise_error_from_response(r, GitlabTodoError, [201, 304]) + + def time_stats(self, **kwargs): + """Get time stats for the merge request. + + Raises: + GitlabConnectionError: If the server cannot be reached. + """ + url = ('/projects/%(project_id)s/merge_requests/%(mr_id)s/time_stats' % + {'project_id': self.project_id, 'mr_id': self.id}) + r = self.gitlab._raw_get(url, **kwargs) + raise_error_from_response(r, GitlabGetError) + return r.json() + + def time_estimate(self, **kwargs): + """Set an estimated time of work for the merge request. + + Raises: + GitlabConnectionError: If the server cannot be reached. + """ + url = ('/projects/%(project_id)s/merge_requests/%(mr_id)s/' + 'time_estimate' % + {'project_id': self.project_id, 'mr_id': self.id}) + r = self.gitlab._raw_post(url, **kwargs) + raise_error_from_response(r, GitlabTimeTrackingError, 201) + return r.json() + + def reset_time_estimate(self, **kwargs): + """Resets estimated time for the merge request to 0 seconds. + + Raises: + GitlabConnectionError: If the server cannot be reached. + """ + url = ('/projects/%(project_id)s/merge_requests/%(mr_id)s/' + 'reset_time_estimate' % + {'project_id': self.project_id, 'mr_id': self.id}) + r = self.gitlab._raw_post(url, **kwargs) + raise_error_from_response(r, GitlabTimeTrackingError, 200) + return r.json() + + def add_spent_time(self, **kwargs): + """Set an estimated time of work for the merge request. + + Raises: + GitlabConnectionError: If the server cannot be reached. + """ + url = ('/projects/%(project_id)s/merge_requests/%(mr_id)s/' + 'add_spent_time' % + {'project_id': self.project_id, 'mr_id': self.id}) + r = self.gitlab._raw_post(url, **kwargs) + raise_error_from_response(r, GitlabTimeTrackingError, 200) + return r.json() + + def reset_spent_time(self, **kwargs): + """Set an estimated time of work for the merge request. + + Raises: + GitlabConnectionError: If the server cannot be reached. + """ + url = ('/projects/%(project_id)s/merge_requests/%(mr_id)s/' + 'reset_spent_time' % + {'project_id': self.project_id, 'mr_id': self.id}) + r = self.gitlab._raw_post(url, **kwargs) + raise_error_from_response(r, GitlabTimeTrackingError, 200) + return r.json() + + +class ProjectMergeRequestManager(BaseManager): + obj_cls = ProjectMergeRequest + + +class ProjectMilestone(GitlabObject): + _url = '/projects/%(project_id)s/milestones' + canDelete = False + requiredUrlAttrs = ['project_id'] + optionalListAttrs = ['iid', 'state'] + requiredCreateAttrs = ['title'] + optionalCreateAttrs = ['description', 'due_date', 'start_date', + 'state_event'] + optionalUpdateAttrs = requiredCreateAttrs + optionalCreateAttrs + shortPrintAttr = 'title' + + def issues(self, **kwargs): + url = "/projects/%s/milestones/%s/issues" % (self.project_id, self.id) + return self.gitlab._raw_list(url, ProjectIssue, + {'project_id': self.project_id}, + **kwargs) + + def merge_requests(self, **kwargs): + """List the merge requests related to this milestone + + Returns: + list (ProjectMergeRequest): List of merge requests + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabListError: If the server fails to perform the request. + """ + url = ('/projects/%s/milestones/%s/merge_requests' % + (self.project_id, self.id)) + return self.gitlab._raw_list(url, ProjectMergeRequest, + {'project_id': self.project_id}, + **kwargs) + + +class ProjectMilestoneManager(BaseManager): + obj_cls = ProjectMilestone + + +class ProjectLabel(GitlabObject): + _url = '/projects/%(project_id)s/labels' + _id_in_delete_url = False + _id_in_update_url = False + canGet = 'from_list' + requiredUrlAttrs = ['project_id'] + idAttr = 'name' + requiredDeleteAttrs = ['name'] + requiredCreateAttrs = ['name', 'color'] + optionalCreateAttrs = ['description', 'priority'] + requiredUpdateAttrs = ['name'] + optionalUpdateAttrs = ['new_name', 'color', 'description', 'priority'] + + def subscribe(self, **kwargs): + """Subscribe to a label. + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabSubscribeError: If the subscription cannot be done + """ + url = ('/projects/%(project_id)s/labels/%(label_id)s/subscription' % + {'project_id': self.project_id, 'label_id': self.name}) + + r = self.gitlab._raw_post(url, **kwargs) + raise_error_from_response(r, GitlabSubscribeError, [201, 304]) + self._set_from_dict(r.json()) + + def unsubscribe(self, **kwargs): + """Unsubscribe a label. + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabUnsubscribeError: If the unsubscription cannot be done + """ + url = ('/projects/%(project_id)s/labels/%(label_id)s/subscription' % + {'project_id': self.project_id, 'label_id': self.name}) + + r = self.gitlab._raw_delete(url, **kwargs) + raise_error_from_response(r, GitlabUnsubscribeError, [200, 304]) + self._set_from_dict(r.json()) + + +class ProjectLabelManager(BaseManager): + obj_cls = ProjectLabel + + +class ProjectFile(GitlabObject): + _url = '/projects/%(project_id)s/repository/files' + canList = False + requiredUrlAttrs = ['project_id'] + requiredGetAttrs = ['file_path', 'ref'] + requiredCreateAttrs = ['file_path', 'branch_name', 'content', + 'commit_message'] + optionalCreateAttrs = ['encoding'] + requiredDeleteAttrs = ['branch_name', 'commit_message', 'file_path'] + shortPrintAttr = 'file_path' + getRequiresId = False + + def decode(self): + """Returns the decoded content of the file. + + Returns: + (str): the decoded content. + """ + return base64.b64decode(self.content) + + +class ProjectFileManager(BaseManager): + obj_cls = ProjectFile + + +class ProjectPipeline(GitlabObject): + _url = '/projects/%(project_id)s/pipelines' + _create_url = '/projects/%(project_id)s/pipeline' + + canUpdate = False + canDelete = False + + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['ref'] + + def retry(self, **kwargs): + """Retries failed builds in a pipeline. + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabPipelineRetryError: If the retry cannot be done. + """ + url = ('/projects/%(project_id)s/pipelines/%(id)s/retry' % + {'project_id': self.project_id, 'id': self.id}) + r = self.gitlab._raw_post(url, data=None, content_type=None, **kwargs) + raise_error_from_response(r, GitlabPipelineRetryError, 201) + self._set_from_dict(r.json()) + + def cancel(self, **kwargs): + """Cancel builds in a pipeline. + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabPipelineCancelError: If the retry cannot be done. + """ + url = ('/projects/%(project_id)s/pipelines/%(id)s/cancel' % + {'project_id': self.project_id, 'id': self.id}) + r = self.gitlab._raw_post(url, data=None, content_type=None, **kwargs) + raise_error_from_response(r, GitlabPipelineRetryError, 200) + self._set_from_dict(r.json()) + + +class ProjectPipelineManager(BaseManager): + obj_cls = ProjectPipeline + + +class ProjectSnippetNote(GitlabObject): + _url = '/projects/%(project_id)s/snippets/%(snippet_id)s/notes' + _constructorTypes = {'author': 'User'} + canUpdate = False + canDelete = False + requiredUrlAttrs = ['project_id', 'snippet_id'] + requiredCreateAttrs = ['body'] + + +class ProjectSnippetNoteManager(BaseManager): + obj_cls = ProjectSnippetNote + + +class ProjectSnippet(GitlabObject): + _url = '/projects/%(project_id)s/snippets' + _constructorTypes = {'author': 'User'} + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['title', 'file_name', 'code'] + optionalCreateAttrs = ['lifetime', 'visibility_level'] + optionalUpdateAttrs = ['title', 'file_name', 'code', 'visibility_level'] + shortPrintAttr = 'title' + managers = ( + ('notes', 'ProjectSnippetNoteManager', + [('project_id', 'project_id'), ('snippet_id', 'id')]), + ) + + def content(self, streamed=False, action=None, chunk_size=1024, **kwargs): + """Return the raw content of a snippet. + + Args: + streamed (bool): If True the data will be processed by chunks of + `chunk_size` and each chunk is passed to `action` for + treatment. + action (callable): Callable responsible of dealing with chunk of + data. + chunk_size (int): Size of each chunk. + + Returns: + str: The snippet content + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabGetError: If the server fails to perform the request. + """ + url = ("/projects/%(project_id)s/snippets/%(snippet_id)s/raw" % + {'project_id': self.project_id, 'snippet_id': self.id}) + r = self.gitlab._raw_get(url, **kwargs) + raise_error_from_response(r, GitlabGetError) + return utils.response_content(r, streamed, action, chunk_size) + + +class ProjectSnippetManager(BaseManager): + obj_cls = ProjectSnippet + + +class ProjectTrigger(GitlabObject): + _url = '/projects/%(project_id)s/triggers' + canUpdate = False + idAttr = 'token' + requiredUrlAttrs = ['project_id'] + + +class ProjectTriggerManager(BaseManager): + obj_cls = ProjectTrigger + + +class ProjectVariable(GitlabObject): + _url = '/projects/%(project_id)s/variables' + idAttr = 'key' + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['key', 'value'] + + +class ProjectVariableManager(BaseManager): + obj_cls = ProjectVariable + + +class ProjectService(GitlabObject): + _url = '/projects/%(project_id)s/services/%(service_name)s' + canList = False + canCreate = False + _id_in_update_url = False + _id_in_delete_url = False + getRequiresId = False + requiredUrlAttrs = ['project_id', 'service_name'] + + _service_attrs = { + 'asana': (('api_key', ), ('restrict_to_branch', )), + 'assembla': (('token', ), ('subdomain', )), + 'bamboo': (('bamboo_url', 'build_key', 'username', 'password'), + tuple()), + 'buildkite': (('token', 'project_url'), ('enable_ssl_verification', )), + 'campfire': (('token', ), ('subdomain', 'room')), + 'custom-issue-tracker': (('new_issue_url', 'issues_url', + 'project_url'), + ('description', 'title')), + 'drone-ci': (('token', 'drone_url'), ('enable_ssl_verification', )), + 'emails-on-push': (('recipients', ), ('disable_diffs', + 'send_from_committer_email')), + 'builds-email': (('recipients', ), ('add_pusher', + 'notify_only_broken_builds')), + 'pipelines-email': (('recipients', ), ('add_pusher', + 'notify_only_broken_builds')), + 'external-wiki': (('external_wiki_url', ), tuple()), + 'flowdock': (('token', ), tuple()), + 'gemnasium': (('api_key', 'token', ), tuple()), + 'hipchat': (('token', ), ('color', 'notify', 'room', 'api_version', + 'server')), + 'irker': (('recipients', ), ('default_irc_uri', 'server_port', + 'server_host', 'colorize_messages')), + 'jira': (tuple(), ( + # Required fields in GitLab >= 8.14 + 'url', 'project_key', + + # Required fields in GitLab < 8.14 + 'new_issue_url', 'project_url', 'issues_url', 'api_url', + 'description', + + # Optional fields + 'username', 'password', 'jira_issue_transition_id')), + 'pivotaltracker': (('token', ), tuple()), + 'pushover': (('api_key', 'user_key', 'priority'), ('device', 'sound')), + 'redmine': (('new_issue_url', 'project_url', 'issues_url'), + ('description', )), + 'slack': (('webhook', ), ('username', 'channel')), + 'teamcity': (('teamcity_url', 'build_type', 'username', 'password'), + tuple()) + } + + def _data_for_gitlab(self, extra_parameters={}, update=False, + as_json=True): + data = (super(ProjectService, self) + ._data_for_gitlab(extra_parameters, update=update, + as_json=False)) + missing = [] + # Mandatory args + for attr in self._service_attrs[self.service_name][0]: + if not hasattr(self, attr): + missing.append(attr) + else: + data[attr] = getattr(self, attr) + + if missing: + raise GitlabUpdateError('Missing attribute(s): %s' % + ", ".join(missing)) + + # Optional args + for attr in self._service_attrs[self.service_name][1]: + if hasattr(self, attr): + data[attr] = getattr(self, attr) + + return json.dumps(data) + + +class ProjectServiceManager(BaseManager): + obj_cls = ProjectService + + def available(self, **kwargs): + """List the services known by python-gitlab. + + Returns: + list (str): The list of service code names. + """ + return list(ProjectService._service_attrs.keys()) + + +class ProjectAccessRequest(GitlabObject): + _url = '/projects/%(project_id)s/access_requests' + canGet = 'from_list' + canUpdate = False + + def approve(self, access_level=gitlab.DEVELOPER_ACCESS, **kwargs): + """Approve an access request. + + Attrs: + access_level (int): The access level for the user. + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabUpdateError: If the server fails to perform the request. + """ + + url = ('/projects/%(project_id)s/access_requests/%(id)s/approve' % + {'project_id': self.project_id, 'id': self.id}) + data = {'access_level': access_level} + r = self.gitlab._raw_put(url, data=data, **kwargs) + raise_error_from_response(r, GitlabUpdateError, 201) + self._set_from_dict(r.json()) + + +class ProjectAccessRequestManager(BaseManager): + obj_cls = ProjectAccessRequest + + +class ProjectDeployment(GitlabObject): + _url = '/projects/%(project_id)s/deployments' + canCreate = False + canUpdate = False + canDelete = False + + +class ProjectDeploymentManager(BaseManager): + obj_cls = ProjectDeployment + + +class ProjectRunner(GitlabObject): + _url = '/projects/%(project_id)s/runners' + canUpdate = False + requiredCreateAttrs = ['runner_id'] + + +class ProjectRunnerManager(BaseManager): + obj_cls = ProjectRunner + + +class Project(GitlabObject): + _url = '/projects' + _constructorTypes = {'owner': 'User', 'namespace': 'Group'} + optionalListAttrs = ['search'] + requiredCreateAttrs = ['name'] + optionalListAttrs = ['search'] + optionalCreateAttrs = ['path', 'namespace_id', 'description', + 'issues_enabled', 'merge_requests_enabled', + 'builds_enabled', 'wiki_enabled', + 'snippets_enabled', 'container_registry_enabled', + 'shared_runners_enabled', 'public', + 'visibility_level', 'import_url', 'public_builds', + 'only_allow_merge_if_build_succeeds', + 'only_allow_merge_if_all_discussions_are_resolved', + 'lfs_enabled', 'request_access_enabled'] + optionalUpdateAttrs = ['name', 'path', 'default_branch', 'description', + 'issues_enabled', 'merge_requests_enabled', + 'builds_enabled', 'wiki_enabled', + 'snippets_enabled', 'container_registry_enabled', + 'shared_runners_enabled', 'public', + 'visibility_level', 'import_url', 'public_builds', + 'only_allow_merge_if_build_succeeds', + 'only_allow_merge_if_all_discussions_are_resolved', + 'lfs_enabled', 'request_access_enabled'] + shortPrintAttr = 'path' + managers = ( + ('accessrequests', 'ProjectAccessRequestManager', + [('project_id', 'id')]), + ('boards', 'ProjectBoardManager', [('project_id', 'id')]), + ('board_lists', 'ProjectBoardListManager', [('project_id', 'id')]), + ('branches', 'ProjectBranchManager', [('project_id', 'id')]), + ('builds', 'ProjectBuildManager', [('project_id', 'id')]), + ('commits', 'ProjectCommitManager', [('project_id', 'id')]), + ('deployments', 'ProjectDeploymentManager', [('project_id', 'id')]), + ('environments', 'ProjectEnvironmentManager', [('project_id', 'id')]), + ('events', 'ProjectEventManager', [('project_id', 'id')]), + ('files', 'ProjectFileManager', [('project_id', 'id')]), + ('forks', 'ProjectForkManager', [('project_id', 'id')]), + ('hooks', 'ProjectHookManager', [('project_id', 'id')]), + ('keys', 'ProjectKeyManager', [('project_id', 'id')]), + ('issues', 'ProjectIssueManager', [('project_id', 'id')]), + ('labels', 'ProjectLabelManager', [('project_id', 'id')]), + ('members', 'ProjectMemberManager', [('project_id', 'id')]), + ('mergerequests', 'ProjectMergeRequestManager', + [('project_id', 'id')]), + ('milestones', 'ProjectMilestoneManager', [('project_id', 'id')]), + ('notes', 'ProjectNoteManager', [('project_id', 'id')]), + ('notificationsettings', 'ProjectNotificationSettingsManager', + [('project_id', 'id')]), + ('pipelines', 'ProjectPipelineManager', [('project_id', 'id')]), + ('runners', 'ProjectRunnerManager', [('project_id', 'id')]), + ('services', 'ProjectServiceManager', [('project_id', 'id')]), + ('snippets', 'ProjectSnippetManager', [('project_id', 'id')]), + ('tags', 'ProjectTagManager', [('project_id', 'id')]), + ('triggers', 'ProjectTriggerManager', [('project_id', 'id')]), + ('variables', 'ProjectVariableManager', [('project_id', 'id')]), + ) + + VISIBILITY_PRIVATE = gitlab.VISIBILITY_PRIVATE + VISIBILITY_INTERNAL = gitlab.VISIBILITY_INTERNAL + VISIBILITY_PUBLIC = gitlab.VISIBILITY_PUBLIC + + def repository_tree(self, path='', ref_name='', **kwargs): + """Return a list of files in the repository. + + Args: + path (str): Path of the top folder (/ by default) + ref_name (str): Reference to a commit or branch + + Returns: + str: The json representation of the tree. + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabGetError: If the server fails to perform the request. + """ + url = "/projects/%s/repository/tree" % (self.id) + params = [] + if path: + params.append(urllib.urlencode({'path': path})) + if ref_name: + params.append("ref_name=%s" % ref_name) + if params: + url += '?' + "&".join(params) + r = self.gitlab._raw_get(url, **kwargs) + raise_error_from_response(r, GitlabGetError) + return r.json() + + def repository_blob(self, sha, filepath, streamed=False, action=None, + chunk_size=1024, **kwargs): + """Return the content of a file for a commit. + + Args: + sha (str): ID of the commit + filepath (str): Path of the file to return + streamed (bool): If True the data will be processed by chunks of + `chunk_size` and each chunk is passed to `action` for + treatment. + action (callable): Callable responsible of dealing with chunk of + data. + chunk_size (int): Size of each chunk. + + Returns: + str: The file content + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabGetError: If the server fails to perform the request. + """ + url = "/projects/%s/repository/blobs/%s" % (self.id, sha) + url += '?%s' % (urllib.urlencode({'filepath': filepath})) + r = self.gitlab._raw_get(url, streamed=streamed, **kwargs) + raise_error_from_response(r, GitlabGetError) + return utils.response_content(r, streamed, action, chunk_size) + + def repository_raw_blob(self, sha, streamed=False, action=None, + chunk_size=1024, **kwargs): + """Returns the raw file contents for a blob by blob SHA. + + Args: + sha(str): ID of the blob + streamed (bool): If True the data will be processed by chunks of + `chunk_size` and each chunk is passed to `action` for + treatment. + action (callable): Callable responsible of dealing with chunk of + data. + chunk_size (int): Size of each chunk. + + Returns: + str: The blob content + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabGetError: If the server fails to perform the request. + """ + url = "/projects/%s/repository/raw_blobs/%s" % (self.id, sha) + r = self.gitlab._raw_get(url, streamed=streamed, **kwargs) + raise_error_from_response(r, GitlabGetError) + return utils.response_content(r, streamed, action, chunk_size) + + def repository_compare(self, from_, to, **kwargs): + """Returns a diff between two branches/commits. + + Args: + from_(str): orig branch/SHA + to(str): dest branch/SHA + + Returns: + str: The diff + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabGetError: If the server fails to perform the request. + """ + url = "/projects/%s/repository/compare" % self.id + url = "%s?from=%s&to=%s" % (url, from_, to) + r = self.gitlab._raw_get(url, **kwargs) + raise_error_from_response(r, GitlabGetError) + return r.json() + + def repository_contributors(self): + """Returns a list of contributors for the project. + + Returns: + list: The contibutors + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabGetError: If the server fails to perform the request. + """ + url = "/projects/%s/repository/contributors" % self.id + r = self.gitlab._raw_get(url) + raise_error_from_response(r, GitlabListError) + return r.json() + + def repository_archive(self, sha=None, streamed=False, action=None, + chunk_size=1024, **kwargs): + """Return a tarball of the repository. + + Args: + sha (str): ID of the commit (default branch by default). + streamed (bool): If True the data will be processed by chunks of + `chunk_size` and each chunk is passed to `action` for + treatment. + action (callable): Callable responsible of dealing with chunk of + data. + chunk_size (int): Size of each chunk. + + Returns: + str: The binary data of the archive. + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabGetError: If the server fails to perform the request. + """ + url = '/projects/%s/repository/archive' % self.id + if sha: + url += '?sha=%s' % sha + r = self.gitlab._raw_get(url, streamed=streamed, **kwargs) + raise_error_from_response(r, GitlabGetError) + return utils.response_content(r, streamed, action, chunk_size) + + def create_fork_relation(self, forked_from_id): + """Create a forked from/to relation between existing projects. + + Args: + forked_from_id (int): The ID of the project that was forked from + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabCreateError: If the server fails to perform the request. + """ + url = "/projects/%s/fork/%s" % (self.id, forked_from_id) + r = self.gitlab._raw_post(url) + raise_error_from_response(r, GitlabCreateError, 201) + + def delete_fork_relation(self): + """Delete a forked relation between existing projects. + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabDeleteError: If the server fails to perform the request. + """ + url = "/projects/%s/fork" % self.id + r = self.gitlab._raw_delete(url) + raise_error_from_response(r, GitlabDeleteError) + + def star(self, **kwargs): + """Star a project. + + Returns: + Project: the updated Project + + Raises: + GitlabCreateError: If the action cannot be done + GitlabConnectionError: If the server cannot be reached. + """ + url = "/projects/%s/star" % self.id + r = self.gitlab._raw_post(url, **kwargs) + raise_error_from_response(r, GitlabCreateError, [201, 304]) + return Project(self.gitlab, r.json()) if r.status_code == 201 else self + + def unstar(self, **kwargs): + """Unstar a project. + + Returns: + Project: the updated Project + + Raises: + GitlabDeleteError: If the action cannot be done + GitlabConnectionError: If the server cannot be reached. + """ + url = "/projects/%s/star" % self.id + r = self.gitlab._raw_delete(url, **kwargs) + raise_error_from_response(r, GitlabDeleteError, [200, 304]) + return Project(self.gitlab, r.json()) if r.status_code == 200 else self + + def archive(self, **kwargs): + """Archive a project. + + Returns: + Project: the updated Project + + Raises: + GitlabCreateError: If the action cannot be done + GitlabConnectionError: If the server cannot be reached. + """ + url = "/projects/%s/archive" % self.id + r = self.gitlab._raw_post(url, **kwargs) + raise_error_from_response(r, GitlabCreateError, 201) + return Project(self.gitlab, r.json()) if r.status_code == 201 else self + + def archive_(self, **kwargs): + warnings.warn("`archive_()` is deprecated, use `archive()` instead", + DeprecationWarning) + return self.archive(**kwargs) + + def unarchive(self, **kwargs): + """Unarchive a project. + + Returns: + Project: the updated Project + + Raises: + GitlabDeleteError: If the action cannot be done + GitlabConnectionError: If the server cannot be reached. + """ + url = "/projects/%s/unarchive" % self.id + r = self.gitlab._raw_delete(url, **kwargs) + raise_error_from_response(r, GitlabCreateError, 201) + return Project(self.gitlab, r.json()) if r.status_code == 201 else self + + def unarchive_(self, **kwargs): + warnings.warn("`unarchive_()` is deprecated, " + "use `unarchive()` instead", + DeprecationWarning) + return self.unarchive(**kwargs) + + def share(self, group_id, group_access, **kwargs): + """Share the project with a group. + + Args: + group_id (int): ID of the group. + group_access (int): Access level for the group. + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabCreateError: If the server fails to perform the request. + """ + url = "/projects/%s/share" % self.id + data = {'group_id': group_id, 'group_access': group_access} + r = self.gitlab._raw_post(url, data=data, **kwargs) + raise_error_from_response(r, GitlabCreateError, 201) + + def trigger_build(self, ref, token, variables={}, **kwargs): + """Trigger a CI build. + + See https://gitlab.com/help/ci/triggers/README.md#trigger-a-build + + Args: + ref (str): Commit to build; can be a commit SHA, a branch name, ... + token (str): The trigger token + variables (dict): Variables passed to the build script + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabCreateError: If the server fails to perform the request. + """ + url = "/projects/%s/trigger/builds" % self.id + form = {r'variables[%s]' % k: v for k, v in six.iteritems(variables)} + data = {'ref': ref, 'token': token} + data.update(form) + r = self.gitlab._raw_post(url, data=data, **kwargs) + raise_error_from_response(r, GitlabCreateError, 201) + + +class Runner(GitlabObject): + _url = '/runners' + canCreate = False + optionalUpdateAttrs = ['description', 'active', 'tag_list'] + optionalListAttrs = ['scope'] + + +class RunnerManager(BaseManager): + obj_cls = Runner + + def all(self, scope=None, **kwargs): + """List all the runners. + + Args: + scope (str): The scope of runners to show, one of: specific, + shared, active, paused, online + + Returns: + list(Runner): a list of runners matching the scope. + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabListError: If the resource cannot be found + """ + url = '/runners/all' + if scope is not None: + url += '?scope=' + scope + return self.gitlab._raw_list(url, self.obj_cls, **kwargs) + + +class TeamMember(GitlabObject): + _url = '/user_teams/%(team_id)s/members' + canUpdate = False + requiredUrlAttrs = ['teamd_id'] + requiredCreateAttrs = ['access_level'] + shortPrintAttr = 'username' + + +class Todo(GitlabObject): + _url = '/todos' + canGet = 'from_list' + canUpdate = False + canCreate = False + optionalListAttrs = ['action', 'author_id', 'project_id', 'state', 'type'] + + +class TodoManager(BaseManager): + obj_cls = Todo + + def delete_all(self, **kwargs): + """Mark all the todos as done. + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabDeleteError: If the resource cannot be found + + Returns: + The number of todos maked done. + """ + url = '/todos' + r = self.gitlab._raw_delete(url, **kwargs) + raise_error_from_response(r, GitlabDeleteError) + return int(r.text) + + +class ProjectManager(BaseManager): + obj_cls = Project + + def search(self, query, **kwargs): + """Search projects by name. + + API v3 only. + + .. note:: + + The search is only performed on the project name (not on the + namespace or the description). To perform a smarter search, use the + ``search`` argument of the ``list()`` method: + + .. code-block:: python + + gl.projects.list(search=your_search_string) + + Args: + query (str): The query string to send to GitLab for the search. + all (bool): If True, return all the items, without pagination + **kwargs: Additional arguments to send to GitLab. + + Returns: + list(gitlab.Gitlab.Project): A list of matching projects. + """ + if self.gitlab.api_version == '4': + raise NotImplementedError("Not supported by v4 API") + + return self.gitlab._raw_list("/projects/search/" + query, Project, + **kwargs) + + def all(self, **kwargs): + """List all the projects (need admin rights). + + Args: + all (bool): If True, return all the items, without pagination + **kwargs: Additional arguments to send to GitLab. + + Returns: + list(gitlab.Gitlab.Project): The list of projects. + """ + return self.gitlab._raw_list("/projects/all", Project, **kwargs) + + def owned(self, **kwargs): + """List owned projects. + + Args: + all (bool): If True, return all the items, without pagination + **kwargs: Additional arguments to send to GitLab. + + Returns: + list(gitlab.Gitlab.Project): The list of owned projects. + """ + return self.gitlab._raw_list("/projects/owned", Project, **kwargs) + + def starred(self, **kwargs): + """List starred projects. + + Args: + all (bool): If True, return all the items, without pagination + **kwargs: Additional arguments to send to GitLab. + + Returns: + list(gitlab.Gitlab.Project): The list of starred projects. + """ + return self.gitlab._raw_list("/projects/starred", Project, **kwargs) + + +class GroupProject(Project): + _url = '/groups/%(group_id)s/projects' + canGet = 'from_list' + canCreate = False + canDelete = False + canUpdate = False + optionalListAttrs = ['archived', 'visibility', 'order_by', 'sort', + 'search', 'ci_enabled_first'] + + def __init__(self, *args, **kwargs): + Project.__init__(self, *args, **kwargs) + + +class GroupProjectManager(ProjectManager): + obj_cls = GroupProject + + +class Group(GitlabObject): + _url = '/groups' + requiredCreateAttrs = ['name', 'path'] + optionalCreateAttrs = ['description', 'visibility_level', 'parent_id', + 'lfs_enabled', 'request_access_enabled'] + optionalUpdateAttrs = ['name', 'path', 'description', 'visibility_level', + 'lfs_enabled', 'request_access_enabled'] + shortPrintAttr = 'name' + managers = ( + ('accessrequests', 'GroupAccessRequestManager', [('group_id', 'id')]), + ('members', 'GroupMemberManager', [('group_id', 'id')]), + ('notificationsettings', 'GroupNotificationSettingsManager', + [('group_id', 'id')]), + ('projects', 'GroupProjectManager', [('group_id', 'id')]), + ('issues', 'GroupIssueManager', [('group_id', 'id')]), + ) + + GUEST_ACCESS = gitlab.GUEST_ACCESS + REPORTER_ACCESS = gitlab.REPORTER_ACCESS + DEVELOPER_ACCESS = gitlab.DEVELOPER_ACCESS + MASTER_ACCESS = gitlab.MASTER_ACCESS + OWNER_ACCESS = gitlab.OWNER_ACCESS + + VISIBILITY_PRIVATE = gitlab.VISIBILITY_PRIVATE + VISIBILITY_INTERNAL = gitlab.VISIBILITY_INTERNAL + VISIBILITY_PUBLIC = gitlab.VISIBILITY_PUBLIC + + def transfer_project(self, id, **kwargs): + """Transfers a project to this new groups. + + Attrs: + id (int): ID of the project to transfer. + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabTransferProjectError: If the server fails to perform the + request. + """ + url = '/groups/%d/projects/%d' % (self.id, id) + r = self.gitlab._raw_post(url, None, **kwargs) + raise_error_from_response(r, GitlabTransferProjectError, 201) + + +class GroupManager(BaseManager): + obj_cls = Group + + def search(self, query, **kwargs): + """Searches groups by name. + + Args: + query (str): The search string + all (bool): If True, return all the items, without pagination + + Returns: + list(Group): a list of matching groups. + """ + url = '/groups?search=' + query + return self.gitlab._raw_list(url, self.obj_cls, **kwargs) + + +class TeamMemberManager(BaseManager): + obj_cls = TeamMember + + +class TeamProject(GitlabObject): + _url = '/user_teams/%(team_id)s/projects' + _constructorTypes = {'owner': 'User', 'namespace': 'Group'} + canUpdate = False + requiredCreateAttrs = ['greatest_access_level'] + requiredUrlAttrs = ['team_id'] + shortPrintAttr = 'name' + + +class TeamProjectManager(BaseManager): + obj_cls = TeamProject + + +class Team(GitlabObject): + _url = '/user_teams' + shortPrintAttr = 'name' + requiredCreateAttrs = ['name', 'path'] + canUpdate = False + managers = ( + ('members', 'TeamMemberManager', [('team_id', 'id')]), + ('projects', 'TeamProjectManager', [('team_id', 'id')]), + ) + + +class TeamManager(BaseManager): + obj_cls = Team From 3f7e5f3e16a982e13c0d4d6bc15ebc1a153c6a8f Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Tue, 23 May 2017 21:52:16 +0200 Subject: [PATCH 06/32] Add missing base.py file --- gitlab/base.py | 533 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 533 insertions(+) create mode 100644 gitlab/base.py diff --git a/gitlab/base.py b/gitlab/base.py new file mode 100644 index 000000000..aa660b24e --- /dev/null +++ b/gitlab/base.py @@ -0,0 +1,533 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2013-2017 Gauvain Pocentek +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +import copy +import importlib +import itertools +import json +import sys + +import six + +import gitlab +from gitlab.exceptions import * # noqa + + +class jsonEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, GitlabObject): + return obj.as_dict() + elif isinstance(obj, gitlab.Gitlab): + return {'url': obj._url} + return json.JSONEncoder.default(self, obj) + + +class BaseManager(object): + """Base manager class for API operations. + + Managers provide method to manage GitLab API objects, such as retrieval, + listing, creation. + + Inherited class must define the ``obj_cls`` attribute. + + Attributes: + obj_cls (class): class of objects wrapped by this manager. + """ + + obj_cls = None + + def __init__(self, gl, parent=None, args=[]): + """Constructs a manager. + + Args: + gl (gitlab.Gitlab): Gitlab object referencing the GitLab server. + parent (Optional[Manager]): A parent manager. + args (list): A list of tuples defining a link between the + parent/child attributes. + + Raises: + AttributeError: If `obj_cls` is None. + """ + self.gitlab = gl + self.args = args + self.parent = parent + + if self.obj_cls is None: + raise AttributeError("obj_cls must be defined") + + def _set_parent_args(self, **kwargs): + args = copy.copy(kwargs) + if self.parent is not None: + for attr, parent_attr in self.args: + args.setdefault(attr, getattr(self.parent, parent_attr)) + + return args + + def get(self, id=None, **kwargs): + """Get a GitLab object. + + Args: + id: ID of the object to retrieve. + **kwargs: Additional arguments to send to GitLab. + + Returns: + object: An object of class `obj_cls`. + + Raises: + NotImplementedError: If objects cannot be retrieved. + GitlabGetError: If the server fails to perform the request. + """ + args = self._set_parent_args(**kwargs) + if not self.obj_cls.canGet: + raise NotImplementedError + if id is None and self.obj_cls.getRequiresId is True: + raise ValueError('The id argument must be defined.') + return self.obj_cls.get(self.gitlab, id, **args) + + def list(self, **kwargs): + """Get a list of GitLab objects. + + Args: + **kwargs: Additional arguments to send to GitLab. + + Returns: + list[object]: A list of `obj_cls` objects. + + Raises: + NotImplementedError: If objects cannot be listed. + GitlabListError: If the server fails to perform the request. + """ + args = self._set_parent_args(**kwargs) + if not self.obj_cls.canList: + raise NotImplementedError + return self.obj_cls.list(self.gitlab, **args) + + def create(self, data, **kwargs): + """Create a new object of class `obj_cls`. + + Args: + data (dict): The parameters to send to the GitLab server to create + the object. Required and optional arguments are defined in the + `requiredCreateAttrs` and `optionalCreateAttrs` of the + `obj_cls` class. + **kwargs: Additional arguments to send to GitLab. + + Returns: + object: A newly create `obj_cls` object. + + Raises: + NotImplementedError: If objects cannot be created. + GitlabCreateError: If the server fails to perform the request. + """ + args = self._set_parent_args(**kwargs) + if not self.obj_cls.canCreate: + raise NotImplementedError + return self.obj_cls.create(self.gitlab, data, **args) + + def delete(self, id, **kwargs): + """Delete a GitLab object. + + Args: + id: ID of the object to delete. + + Raises: + NotImplementedError: If objects cannot be deleted. + GitlabDeleteError: If the server fails to perform the request. + """ + args = self._set_parent_args(**kwargs) + if not self.obj_cls.canDelete: + raise NotImplementedError + self.gitlab.delete(self.obj_cls, id, **args) + + +class GitlabObject(object): + """Base class for all classes that interface with GitLab.""" + #: Url to use in GitLab for this object + _url = None + # Some objects (e.g. merge requests) have different urls for singular and + # plural + _urlPlural = None + _id_in_delete_url = True + _id_in_update_url = True + _constructorTypes = None + + #: Tells if GitLab-api allows retrieving single objects. + canGet = True + #: Tells if GitLab-api allows listing of objects. + canList = True + #: Tells if GitLab-api allows creation of new objects. + canCreate = True + #: Tells if GitLab-api allows updating object. + canUpdate = True + #: Tells if GitLab-api allows deleting object. + canDelete = True + #: Attributes that are required for constructing url. + requiredUrlAttrs = [] + #: Attributes that are required when retrieving list of objects. + requiredListAttrs = [] + #: Attributes that are optional when retrieving list of objects. + optionalListAttrs = [] + #: Attributes that are optional when retrieving single object. + optionalGetAttrs = [] + #: Attributes that are required when retrieving single object. + requiredGetAttrs = [] + #: Attributes that are required when deleting object. + requiredDeleteAttrs = [] + #: Attributes that are required when creating a new object. + requiredCreateAttrs = [] + #: Attributes that are optional when creating a new object. + optionalCreateAttrs = [] + #: Attributes that are required when updating an object. + requiredUpdateAttrs = [] + #: Attributes that are optional when updating an object. + optionalUpdateAttrs = [] + #: Whether the object ID is required in the GET url. + getRequiresId = True + #: List of managers to create. + managers = [] + #: Name of the identifier of an object. + idAttr = 'id' + #: Attribute to use as ID when displaying the object. + shortPrintAttr = None + + def _data_for_gitlab(self, extra_parameters={}, update=False, + as_json=True): + data = {} + if update and (self.requiredUpdateAttrs or self.optionalUpdateAttrs): + attributes = itertools.chain(self.requiredUpdateAttrs, + self.optionalUpdateAttrs) + else: + attributes = itertools.chain(self.requiredCreateAttrs, + self.optionalCreateAttrs) + attributes = list(attributes) + ['sudo', 'page', 'per_page'] + for attribute in attributes: + if hasattr(self, attribute): + value = getattr(self, attribute) + # labels need to be sent as a comma-separated list + if attribute == 'labels' and isinstance(value, list): + value = ", ".join(value) + elif attribute == 'sudo': + value = str(value) + data[attribute] = value + + data.update(extra_parameters) + + return json.dumps(data) if as_json else data + + @classmethod + def list(cls, gl, **kwargs): + """Retrieve a list of objects from GitLab. + + Args: + gl (gitlab.Gitlab): Gitlab object referencing the GitLab server. + per_page (int): Maximum number of items to return. + page (int): ID of the page to return when using pagination. + + Returns: + list[object]: A list of objects. + + Raises: + NotImplementedError: If objects can't be listed. + GitlabListError: If the server cannot perform the request. + """ + if not cls.canList: + raise NotImplementedError + + if not cls._url: + raise NotImplementedError + + return gl.list(cls, **kwargs) + + @classmethod + def get(cls, gl, id, **kwargs): + """Retrieve a single object. + + Args: + gl (gitlab.Gitlab): Gitlab object referencing the GitLab server. + id (int or str): ID of the object to retrieve. + + Returns: + object: The found GitLab object. + + Raises: + NotImplementedError: If objects can't be retrieved. + GitlabGetError: If the server cannot perform the request. + """ + + if cls.canGet is False: + raise NotImplementedError + elif cls.canGet is True: + return cls(gl, id, **kwargs) + elif cls.canGet == 'from_list': + for obj in cls.list(gl, **kwargs): + obj_id = getattr(obj, obj.idAttr) + if str(obj_id) == str(id): + return obj + + raise GitlabGetError("Object not found") + + def _get_object(self, k, v, **kwargs): + if self._constructorTypes and k in self._constructorTypes: + cls = getattr(self._module, self._constructorTypes[k]) + return cls(self.gitlab, v, **kwargs) + else: + return v + + def _set_from_dict(self, data, **kwargs): + if not hasattr(data, 'items'): + return + + for k, v in data.items(): + # If a k attribute already exists and is a Manager, do nothing (see + # https://github.com/python-gitlab/python-gitlab/issues/209) + if isinstance(getattr(self, k, None), BaseManager): + continue + + if isinstance(v, list): + self.__dict__[k] = [] + for i in v: + self.__dict__[k].append(self._get_object(k, i, **kwargs)) + elif v is None: + self.__dict__[k] = None + else: + self.__dict__[k] = self._get_object(k, v, **kwargs) + + def _create(self, **kwargs): + if not self.canCreate: + raise NotImplementedError + + json = self.gitlab.create(self, **kwargs) + self._set_from_dict(json) + self._from_api = True + + def _update(self, **kwargs): + if not self.canUpdate: + raise NotImplementedError + + json = self.gitlab.update(self, **kwargs) + self._set_from_dict(json) + + def save(self, **kwargs): + if self._from_api: + self._update(**kwargs) + else: + self._create(**kwargs) + + def delete(self, **kwargs): + if not self.canDelete: + raise NotImplementedError + + if not self._from_api: + raise GitlabDeleteError("Object not yet created") + + return self.gitlab.delete(self, **kwargs) + + @classmethod + def create(cls, gl, data, **kwargs): + """Create an object. + + Args: + gl (gitlab.Gitlab): Gitlab object referencing the GitLab server. + data (dict): The data used to define the object. + + Returns: + object: The new object. + + Raises: + NotImplementedError: If objects can't be created. + GitlabCreateError: If the server cannot perform the request. + """ + if not cls.canCreate: + raise NotImplementedError + + obj = cls(gl, data, **kwargs) + obj.save() + + return obj + + def __init__(self, gl, data=None, **kwargs): + """Constructs a new object. + + Do not use this method. Use the `get` or `create` class methods + instead. + + Args: + gl (gitlab.Gitlab): Gitlab object referencing the GitLab server. + data: If `data` is a dict, create a new object using the + information. If it is an int or a string, get a GitLab object + from an API request. + **kwargs: Additional arguments to send to GitLab. + """ + self._from_api = False + #: (gitlab.Gitlab): Gitlab connection. + self.gitlab = gl + + # store the module in which the object has been created (v3/v4) to be + # able to reference other objects from the same module + self._module = importlib.import_module(self.__module__) + + if (data is None or isinstance(data, six.integer_types) or + isinstance(data, six.string_types)): + if not self.canGet: + raise NotImplementedError + data = self.gitlab.get(self.__class__, data, **kwargs) + self._from_api = True + + # the API returned a list because custom kwargs where used + # instead of the id to request an object. Usually parameters + # other than an id return ambiguous results. However in the + # gitlab universe iids together with a project_id are + # unambiguous for merge requests and issues, too. + # So if there is only one element we can use it as our data + # source. + if 'iid' in kwargs and isinstance(data, list): + if len(data) < 1: + raise GitlabGetError('Not found') + elif len(data) == 1: + data = data[0] + else: + raise GitlabGetError('Impossible! You found multiple' + ' elements with the same iid.') + + self._set_from_dict(data, **kwargs) + + if kwargs: + for k, v in kwargs.items(): + # Don't overwrite attributes returned by the server (#171) + if k not in self.__dict__ or not self.__dict__[k]: + self.__dict__[k] = v + + # Special handling for api-objects that don't have id-number in api + # responses. Currently only Labels and Files + if not hasattr(self, "id"): + self.id = None + + def _set_manager(self, var, cls, attrs): + manager = cls(self.gitlab, self, attrs) + setattr(self, var, manager) + + def __getattr__(self, name): + # build a manager if it doesn't exist yet + for var, cls, attrs in self.managers: + if var != name: + continue + # Build the full class path if needed + if isinstance(cls, six.string_types): + cls = getattr(self._module, cls) + self._set_manager(var, cls, attrs) + return getattr(self, var) + + raise AttributeError + + def __str__(self): + return '%s => %s' % (type(self), str(self.__dict__)) + + def __repr__(self): + return '<%s %s:%s>' % (self.__class__.__name__, + self.idAttr, + getattr(self, self.idAttr)) + + def display(self, pretty): + if pretty: + self.pretty_print() + else: + self.short_print() + + def short_print(self, depth=0): + """Print the object on the standard output (verbose). + + Args: + depth (int): Used internaly for recursive call. + """ + id = self.__dict__[self.idAttr] + print("%s%s: %s" % (" " * depth * 2, self.idAttr, id)) + if self.shortPrintAttr: + print("%s%s: %s" % (" " * depth * 2, + self.shortPrintAttr.replace('_', '-'), + self.__dict__[self.shortPrintAttr])) + + @staticmethod + def _get_display_encoding(): + return sys.stdout.encoding or sys.getdefaultencoding() + + @staticmethod + def _obj_to_str(obj): + if isinstance(obj, dict): + s = ", ".join(["%s: %s" % + (x, GitlabObject._obj_to_str(y)) + for (x, y) in obj.items()]) + return "{ %s }" % s + elif isinstance(obj, list): + s = ", ".join([GitlabObject._obj_to_str(x) for x in obj]) + return "[ %s ]" % s + elif six.PY2 and isinstance(obj, six.text_type): + return obj.encode(GitlabObject._get_display_encoding(), "replace") + else: + return str(obj) + + def pretty_print(self, depth=0): + """Print the object on the standard output (verbose). + + Args: + depth (int): Used internaly for recursive call. + """ + id = self.__dict__[self.idAttr] + print("%s%s: %s" % (" " * depth * 2, self.idAttr, id)) + for k in sorted(self.__dict__.keys()): + if k in (self.idAttr, 'id', 'gitlab'): + continue + if k[0] == '_': + continue + v = self.__dict__[k] + pretty_k = k.replace('_', '-') + if six.PY2: + pretty_k = pretty_k.encode( + GitlabObject._get_display_encoding(), "replace") + if isinstance(v, GitlabObject): + if depth == 0: + print("%s:" % pretty_k) + v.pretty_print(1) + else: + print("%s: %s" % (pretty_k, v.id)) + elif isinstance(v, BaseManager): + continue + else: + if hasattr(v, __name__) and v.__name__ == 'Gitlab': + continue + v = GitlabObject._obj_to_str(v) + print("%s%s: %s" % (" " * depth * 2, pretty_k, v)) + + def json(self): + """Dump the object as json. + + Returns: + str: The json string. + """ + return json.dumps(self, cls=jsonEncoder) + + def as_dict(self): + """Dump the object as a dict.""" + return {k: v for k, v in six.iteritems(self.__dict__) + if (not isinstance(v, BaseManager) and not k[0] == '_')} + + def __eq__(self, other): + if type(other) is type(self): + return self.as_dict() == other.as_dict() + return False + + def __ne__(self, other): + return not self.__eq__(other) From 17dffdffdc638111d0652526fcaf17f373ed1ee3 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Tue, 23 May 2017 21:52:40 +0200 Subject: [PATCH 07/32] [v4] Drop teams support --- gitlab/__init__.py | 3 ++- gitlab/v4/objects.py | 40 ---------------------------------------- 2 files changed, 2 insertions(+), 41 deletions(-) diff --git a/gitlab/__init__.py b/gitlab/__init__.py index b3f6dcd15..d4e7336d6 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -112,7 +112,8 @@ def __init__(self, url, private_token=None, email=None, password=None, self.sidekiq = objects.SidekiqManager(self) self.snippets = objects.SnippetManager(self) self.users = objects.UserManager(self) - self.teams = objects.TeamManager(self) + if self._api_version == '3': + self.teams = objects.TeamManager(self) self.todos = objects.TodoManager(self) # build the "submanagers" diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index 01bb67040..0bfacc5cd 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -2138,14 +2138,6 @@ def all(self, scope=None, **kwargs): return self.gitlab._raw_list(url, self.obj_cls, **kwargs) -class TeamMember(GitlabObject): - _url = '/user_teams/%(team_id)s/members' - canUpdate = False - requiredUrlAttrs = ['teamd_id'] - requiredCreateAttrs = ['access_level'] - shortPrintAttr = 'username' - - class Todo(GitlabObject): _url = '/todos' canGet = 'from_list' @@ -2317,35 +2309,3 @@ def search(self, query, **kwargs): """ url = '/groups?search=' + query return self.gitlab._raw_list(url, self.obj_cls, **kwargs) - - -class TeamMemberManager(BaseManager): - obj_cls = TeamMember - - -class TeamProject(GitlabObject): - _url = '/user_teams/%(team_id)s/projects' - _constructorTypes = {'owner': 'User', 'namespace': 'Group'} - canUpdate = False - requiredCreateAttrs = ['greatest_access_level'] - requiredUrlAttrs = ['team_id'] - shortPrintAttr = 'name' - - -class TeamProjectManager(BaseManager): - obj_cls = TeamProject - - -class Team(GitlabObject): - _url = '/user_teams' - shortPrintAttr = 'name' - requiredCreateAttrs = ['name', 'path'] - canUpdate = False - managers = ( - ('members', 'TeamMemberManager', [('team_id', 'id')]), - ('projects', 'TeamProjectManager', [('team_id', 'id')]), - ) - - -class TeamManager(BaseManager): - obj_cls = Team From af70ec3e2ff17385c4b72fe4d317313e94f5cb0b Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Tue, 23 May 2017 21:53:34 +0200 Subject: [PATCH 08/32] [v4] projects.search() has been removed --- gitlab/v4/objects.py | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index 0bfacc5cd..3cf22c0d2 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -2168,35 +2168,6 @@ def delete_all(self, **kwargs): class ProjectManager(BaseManager): obj_cls = Project - def search(self, query, **kwargs): - """Search projects by name. - - API v3 only. - - .. note:: - - The search is only performed on the project name (not on the - namespace or the description). To perform a smarter search, use the - ``search`` argument of the ``list()`` method: - - .. code-block:: python - - gl.projects.list(search=your_search_string) - - Args: - query (str): The query string to send to GitLab for the search. - all (bool): If True, return all the items, without pagination - **kwargs: Additional arguments to send to GitLab. - - Returns: - list(gitlab.Gitlab.Project): A list of matching projects. - """ - if self.gitlab.api_version == '4': - raise NotImplementedError("Not supported by v4 API") - - return self.gitlab._raw_list("/projects/search/" + query, Project, - **kwargs) - def all(self, **kwargs): """List all the projects (need admin rights). From 92590410a0ce28fbeb984eec066d53f03d8f6212 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Tue, 23 May 2017 21:54:38 +0200 Subject: [PATCH 09/32] [v4] Update iid attr for issues and MRs --- gitlab/v4/objects.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index 3cf22c0d2..6987da881 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -921,8 +921,7 @@ class ProjectIssue(GitlabObject): _url = '/projects/%(project_id)s/issues/' _constructorTypes = {'author': 'User', 'assignee': 'User', 'milestone': 'ProjectMilestone'} - optionalListAttrs = ['state', 'labels', 'milestone', 'iid', 'order_by', - 'sort'] + optionalListAttrs = ['state', 'labels', 'milestone', 'order_by', 'sort'] requiredUrlAttrs = ['project_id'] requiredCreateAttrs = ['title'] optionalCreateAttrs = ['description', 'assignee_id', 'milestone_id', @@ -1178,7 +1177,7 @@ class ProjectMergeRequest(GitlabObject): optionalUpdateAttrs = ['target_branch', 'assignee_id', 'title', 'description', 'state_event', 'labels', 'milestone_id'] - optionalListAttrs = ['iid', 'state', 'order_by', 'sort'] + optionalListAttrs = ['iids', 'state', 'order_by', 'sort'] managers = ( ('notes', 'ProjectMergeRequestNoteManager', From d71800bb2d7ea4427da75105e7830082d2d832f0 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Tue, 23 May 2017 21:56:20 +0200 Subject: [PATCH 10/32] [v4] Update project keys endpoint --- gitlab/v4/objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index 6987da881..d781fe4df 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -839,7 +839,7 @@ class ProjectEnvironmentManager(BaseManager): class ProjectKey(GitlabObject): - _url = '/projects/%(project_id)s/keys' + _url = '/projects/%(project_id)s/deploy_keys' canUpdate = False requiredUrlAttrs = ['project_id'] requiredCreateAttrs = ['title', 'key'] From 76ca2345ec3019a440696b59861d40333e2a1353 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Tue, 23 May 2017 21:59:48 +0200 Subject: [PATCH 11/32] [v4] Update project unstar endpoint --- gitlab/v4/objects.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index d781fe4df..c187fa320 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -2023,10 +2023,10 @@ def unstar(self, **kwargs): GitlabDeleteError: If the action cannot be done GitlabConnectionError: If the server cannot be reached. """ - url = "/projects/%s/star" % self.id - r = self.gitlab._raw_delete(url, **kwargs) - raise_error_from_response(r, GitlabDeleteError, [200, 304]) - return Project(self.gitlab, r.json()) if r.status_code == 200 else self + url = "/projects/%s/unstar" % self.id + r = self.gitlab._raw_post(url, **kwargs) + raise_error_from_response(r, GitlabDeleteError, [201, 304]) + return Project(self.gitlab, r.json()) if r.status_code == 201 else self def archive(self, **kwargs): """Archive a project. From 206be8f517d9b477ee217e8102647df7efa120da Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Tue, 23 May 2017 22:02:19 +0200 Subject: [PATCH 12/32] [v4] Update the licenses templates endpoint --- gitlab/v4/objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index c187fa320..26e64742e 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -472,7 +472,7 @@ class IssueManager(BaseManager): class License(GitlabObject): - _url = '/licenses' + _url = '/templates/licenses' canDelete = False canUpdate = False canCreate = False From 6684c13a4f98b4c4b7c8a6af1957711d7cc0ae2b Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Tue, 23 May 2017 22:06:12 +0200 Subject: [PATCH 13/32] [v4] Update project fork endpoint --- gitlab/v4/objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index 26e64742e..c38b115c3 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -876,7 +876,7 @@ class ProjectEventManager(BaseManager): class ProjectFork(GitlabObject): - _url = '/projects/fork/%(project_id)s' + _url = '/projects/%(project_id)s/fork' canUpdate = False canDelete = False canList = False From e789cee1cd619e9e1b2358915936bccc876879ad Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Tue, 23 May 2017 22:12:02 +0200 Subject: [PATCH 14/32] [v4] Add projects.list() attributes All the ProjectManager filter methods can now be handled by projects.list(). --- gitlab/v4/objects.py | 40 +++------------------------------------- 1 file changed, 3 insertions(+), 37 deletions(-) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index c38b115c3..f38b60bf6 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -1776,7 +1776,9 @@ class Project(GitlabObject): _constructorTypes = {'owner': 'User', 'namespace': 'Group'} optionalListAttrs = ['search'] requiredCreateAttrs = ['name'] - optionalListAttrs = ['search'] + optionalListAttrs = ['search', 'owned', 'starred', 'archived', + 'visibility', 'order_by', 'sort', 'simple', + 'membership', 'statistics'] optionalCreateAttrs = ['path', 'namespace_id', 'description', 'issues_enabled', 'merge_requests_enabled', 'builds_enabled', 'wiki_enabled', @@ -2167,42 +2169,6 @@ def delete_all(self, **kwargs): class ProjectManager(BaseManager): obj_cls = Project - def all(self, **kwargs): - """List all the projects (need admin rights). - - Args: - all (bool): If True, return all the items, without pagination - **kwargs: Additional arguments to send to GitLab. - - Returns: - list(gitlab.Gitlab.Project): The list of projects. - """ - return self.gitlab._raw_list("/projects/all", Project, **kwargs) - - def owned(self, **kwargs): - """List owned projects. - - Args: - all (bool): If True, return all the items, without pagination - **kwargs: Additional arguments to send to GitLab. - - Returns: - list(gitlab.Gitlab.Project): The list of owned projects. - """ - return self.gitlab._raw_list("/projects/owned", Project, **kwargs) - - def starred(self, **kwargs): - """List starred projects. - - Args: - all (bool): If True, return all the items, without pagination - **kwargs: Additional arguments to send to GitLab. - - Returns: - list(gitlab.Gitlab.Project): The list of starred projects. - """ - return self.gitlab._raw_list("/projects/starred", Project, **kwargs) - class GroupProject(Project): _url = '/groups/%(group_id)s/projects' From 41f141d84c6b2790e5d28f476fbfe139be77881e Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Tue, 23 May 2017 22:19:31 +0200 Subject: [PATCH 15/32] [v4] Drop ProjectKeyManager.enable() --- gitlab/v4/objects.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index f38b60bf6..75090e867 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -854,12 +854,6 @@ def enable(self, key_id): r = self.gitlab._raw_post(url) raise_error_from_response(r, GitlabProjectDeployKeyError, 201) - def disable(self, key_id): - """Disable a deploy key for a project.""" - url = '/projects/%s/deploy_keys/%s/disable' % (self.parent.id, key_id) - r = self.gitlab._raw_delete(url) - raise_error_from_response(r, GitlabProjectDeployKeyError, 200) - class ProjectEvent(GitlabObject): _url = '/projects/%(project_id)s/events' From 5c8cb293bca387309b9e40fc6b1a96cc8fbd8dfe Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Tue, 23 May 2017 22:22:42 +0200 Subject: [PATCH 16/32] [v4] Update user (un)block HTTP methods --- gitlab/v4/objects.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index 75090e867..aac7a04f8 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -140,15 +140,15 @@ def _data_for_gitlab(self, extra_parameters={}, update=False, def block(self, **kwargs): """Blocks the user.""" url = '/users/%s/block' % self.id - r = self.gitlab._raw_put(url, **kwargs) - raise_error_from_response(r, GitlabBlockError) + r = self.gitlab._raw_post(url, **kwargs) + raise_error_from_response(r, GitlabBlockError, 201) self.state = 'blocked' def unblock(self, **kwargs): """Unblocks the user.""" url = '/users/%s/unblock' % self.id - r = self.gitlab._raw_put(url, **kwargs) - raise_error_from_response(r, GitlabUnblockError) + r = self.gitlab._raw_post(url, **kwargs) + raise_error_from_response(r, GitlabUnblockError, 201) self.state = 'active' def __eq__(self, other): From 90c895824aaf84a9a77f9a3fd18db6d16b73908d Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Tue, 23 May 2017 22:26:50 +0200 Subject: [PATCH 17/32] [v4] Update (un)subscribtion endpoints --- gitlab/v4/objects.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index aac7a04f8..f46495cc7 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -936,11 +936,11 @@ def subscribe(self, **kwargs): GitlabConnectionError: If the server cannot be reached. GitlabSubscribeError: If the subscription cannot be done """ - url = ('/projects/%(project_id)s/issues/%(issue_id)s/subscription' % + url = ('/projects/%(project_id)s/issues/%(issue_id)s/subscribe' % {'project_id': self.project_id, 'issue_id': self.id}) r = self.gitlab._raw_post(url, **kwargs) - raise_error_from_response(r, GitlabSubscribeError) + raise_error_from_response(r, GitlabSubscribeError, [201, 304]) self._set_from_dict(r.json()) def unsubscribe(self, **kwargs): @@ -950,11 +950,11 @@ def unsubscribe(self, **kwargs): GitlabConnectionError: If the server cannot be reached. GitlabUnsubscribeError: If the unsubscription cannot be done """ - url = ('/projects/%(project_id)s/issues/%(issue_id)s/subscription' % + url = ('/projects/%(project_id)s/issues/%(issue_id)s/unsubscribe' % {'project_id': self.project_id, 'issue_id': self.id}) - r = self.gitlab._raw_delete(url, **kwargs) - raise_error_from_response(r, GitlabUnsubscribeError) + r = self.gitlab._raw_post(url, **kwargs) + raise_error_from_response(r, GitlabUnsubscribeError, [201, 304]) self._set_from_dict(r.json()) def move(self, to_project_id, **kwargs): @@ -1199,7 +1199,7 @@ def subscribe(self, **kwargs): GitlabSubscribeError: If the subscription cannot be done """ url = ('/projects/%(project_id)s/merge_requests/%(mr_id)s/' - 'subscription' % + 'subscribe' % {'project_id': self.project_id, 'mr_id': self.id}) r = self.gitlab._raw_post(url, **kwargs) @@ -1215,11 +1215,11 @@ def unsubscribe(self, **kwargs): GitlabUnsubscribeError: If the unsubscription cannot be done """ url = ('/projects/%(project_id)s/merge_requests/%(mr_id)s/' - 'subscription' % + 'unsubscribe' % {'project_id': self.project_id, 'mr_id': self.id}) - r = self.gitlab._raw_delete(url, **kwargs) - raise_error_from_response(r, GitlabUnsubscribeError, [200, 304]) + r = self.gitlab._raw_post(url, **kwargs) + raise_error_from_response(r, GitlabUnsubscribeError, [201, 304]) if r.status_code == 200: self._set_from_dict(r.json()) @@ -1458,7 +1458,7 @@ def subscribe(self, **kwargs): GitlabConnectionError: If the server cannot be reached. GitlabSubscribeError: If the subscription cannot be done """ - url = ('/projects/%(project_id)s/labels/%(label_id)s/subscription' % + url = ('/projects/%(project_id)s/labels/%(label_id)s/subscribe' % {'project_id': self.project_id, 'label_id': self.name}) r = self.gitlab._raw_post(url, **kwargs) @@ -1472,11 +1472,11 @@ def unsubscribe(self, **kwargs): GitlabConnectionError: If the server cannot be reached. GitlabUnsubscribeError: If the unsubscription cannot be done """ - url = ('/projects/%(project_id)s/labels/%(label_id)s/subscription' % + url = ('/projects/%(project_id)s/labels/%(label_id)s/unsubscribe' % {'project_id': self.project_id, 'label_id': self.name}) - r = self.gitlab._raw_delete(url, **kwargs) - raise_error_from_response(r, GitlabUnsubscribeError, [200, 304]) + r = self.gitlab._raw_post(url, **kwargs) + raise_error_from_response(r, GitlabUnsubscribeError, [201, 304]) self._set_from_dict(r.json()) From 8b75bc8d96878e5d058ebd5ec5c82383a0d92573 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Tue, 23 May 2017 22:28:01 +0200 Subject: [PATCH 18/32] [v4] Rename branch_name to branch --- gitlab/v4/objects.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index f46495cc7..0e929b8f5 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -584,7 +584,7 @@ class ProjectBranch(GitlabObject): idAttr = 'name' canUpdate = False requiredUrlAttrs = ['project_id'] - requiredCreateAttrs = ['branch_name', 'ref'] + requiredCreateAttrs = ['branch', 'ref'] def protect(self, protect=True, **kwargs): """Protects the branch.""" @@ -741,7 +741,7 @@ class ProjectCommit(GitlabObject): canDelete = False canUpdate = False requiredUrlAttrs = ['project_id'] - requiredCreateAttrs = ['branch_name', 'commit_message', 'actions'] + requiredCreateAttrs = ['branch', 'commit_message', 'actions'] optionalCreateAttrs = ['author_email', 'author_name'] shortPrintAttr = 'title' managers = ( @@ -1489,10 +1489,10 @@ class ProjectFile(GitlabObject): canList = False requiredUrlAttrs = ['project_id'] requiredGetAttrs = ['file_path', 'ref'] - requiredCreateAttrs = ['file_path', 'branch_name', 'content', + requiredCreateAttrs = ['file_path', 'branch', 'content', 'commit_message'] optionalCreateAttrs = ['encoding'] - requiredDeleteAttrs = ['branch_name', 'commit_message', 'file_path'] + requiredDeleteAttrs = ['branch', 'commit_message', 'file_path'] shortPrintAttr = 'file_path' getRequiresId = False From 9de53bf8710b826ffcacfb15330469d537add14c Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Tue, 23 May 2017 22:30:44 +0200 Subject: [PATCH 19/32] [v4] MR s/build/pipeline/ in attributes --- gitlab/v4/objects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index 0e929b8f5..36f0df90b 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -1223,10 +1223,10 @@ def unsubscribe(self, **kwargs): if r.status_code == 200: self._set_from_dict(r.json()) - def cancel_merge_when_build_succeeds(self, **kwargs): + def cancel_merge_when_pipeline_succeeds(self, **kwargs): """Cancel merge when build succeeds.""" - u = ('/projects/%s/merge_requests/%s/cancel_merge_when_build_succeeds' + u = ('/projects/%s/merge_requests/%s/cancel_merge_when_pipeline_succeeds' % (self.project_id, self.id)) r = self.gitlab._raw_put(u, **kwargs) errors = {401: GitlabMRForbiddenError, From 9b625f07ec36a073066fa15d2fbf294bf014e62e Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Tue, 23 May 2017 22:32:25 +0200 Subject: [PATCH 20/32] [v4] Remove public attribute for projects --- gitlab/v4/objects.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index 36f0df90b..0406556c4 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -1777,8 +1777,8 @@ class Project(GitlabObject): 'issues_enabled', 'merge_requests_enabled', 'builds_enabled', 'wiki_enabled', 'snippets_enabled', 'container_registry_enabled', - 'shared_runners_enabled', 'public', - 'visibility_level', 'import_url', 'public_builds', + 'shared_runners_enabled', 'visibility_level', + 'import_url', 'public_builds', 'only_allow_merge_if_build_succeeds', 'only_allow_merge_if_all_discussions_are_resolved', 'lfs_enabled', 'request_access_enabled'] @@ -1786,8 +1786,8 @@ class Project(GitlabObject): 'issues_enabled', 'merge_requests_enabled', 'builds_enabled', 'wiki_enabled', 'snippets_enabled', 'container_registry_enabled', - 'shared_runners_enabled', 'public', - 'visibility_level', 'import_url', 'public_builds', + 'shared_runners_enabled', 'visibility_level', + 'import_url', 'public_builds', 'only_allow_merge_if_build_succeeds', 'only_allow_merge_if_all_discussions_are_resolved', 'lfs_enabled', 'request_access_enabled'] From 27c1e954d8fc07325c5e156e0b130e9a4757e7ff Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Tue, 23 May 2017 22:42:27 +0200 Subject: [PATCH 21/32] [v4] Rename the visibility attribute Also change the value of the VISIBILITY_* consts, and move them to the `objects` module root. TODO: deal the numerical value used by v3. --- gitlab/v4/objects.py | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index 0406556c4..af407f2cb 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -30,6 +30,10 @@ from gitlab.exceptions import * # noqa from gitlab import utils +VISIBILITY_PRIVATE = 'private' +VISIBILITY_INTERNAL = 'internal' +VISIBILITY_PUBLIC = 'public' + class SidekiqManager(object): """Manager for the Sidekiq methods. @@ -102,7 +106,7 @@ class UserProject(GitlabObject): requiredCreateAttrs = ['name'] optionalCreateAttrs = ['default_branch', 'issues_enabled', 'wall_enabled', 'merge_requests_enabled', 'wiki_enabled', - 'snippets_enabled', 'public', 'visibility_level', + 'snippets_enabled', 'public', 'visibility', 'description', 'builds_enabled', 'public_builds', 'import_url', 'only_allow_merge_if_build_succeeds'] @@ -490,8 +494,8 @@ class Snippet(GitlabObject): _url = '/snippets' _constructorTypes = {'author': 'User'} requiredCreateAttrs = ['title', 'file_name', 'content'] - optionalCreateAttrs = ['lifetime', 'visibility_level'] - optionalUpdateAttrs = ['title', 'file_name', 'content', 'visibility_level'] + optionalCreateAttrs = ['lifetime', 'visibility'] + optionalUpdateAttrs = ['title', 'file_name', 'content', 'visibility'] shortPrintAttr = 'title' def raw(self, streamed=False, action=None, chunk_size=1024, **kwargs): @@ -1568,8 +1572,8 @@ class ProjectSnippet(GitlabObject): _constructorTypes = {'author': 'User'} requiredUrlAttrs = ['project_id'] requiredCreateAttrs = ['title', 'file_name', 'code'] - optionalCreateAttrs = ['lifetime', 'visibility_level'] - optionalUpdateAttrs = ['title', 'file_name', 'code', 'visibility_level'] + optionalCreateAttrs = ['lifetime', 'visibility'] + optionalUpdateAttrs = ['title', 'file_name', 'code', 'visibility'] shortPrintAttr = 'title' managers = ( ('notes', 'ProjectSnippetNoteManager', @@ -1777,7 +1781,7 @@ class Project(GitlabObject): 'issues_enabled', 'merge_requests_enabled', 'builds_enabled', 'wiki_enabled', 'snippets_enabled', 'container_registry_enabled', - 'shared_runners_enabled', 'visibility_level', + 'shared_runners_enabled', 'visibility', 'import_url', 'public_builds', 'only_allow_merge_if_build_succeeds', 'only_allow_merge_if_all_discussions_are_resolved', @@ -1786,7 +1790,7 @@ class Project(GitlabObject): 'issues_enabled', 'merge_requests_enabled', 'builds_enabled', 'wiki_enabled', 'snippets_enabled', 'container_registry_enabled', - 'shared_runners_enabled', 'visibility_level', + 'shared_runners_enabled', 'visibility', 'import_url', 'public_builds', 'only_allow_merge_if_build_succeeds', 'only_allow_merge_if_all_discussions_are_resolved', @@ -1825,10 +1829,6 @@ class Project(GitlabObject): ('variables', 'ProjectVariableManager', [('project_id', 'id')]), ) - VISIBILITY_PRIVATE = gitlab.VISIBILITY_PRIVATE - VISIBILITY_INTERNAL = gitlab.VISIBILITY_INTERNAL - VISIBILITY_PUBLIC = gitlab.VISIBILITY_PUBLIC - def repository_tree(self, path='', ref_name='', **kwargs): """Return a list of files in the repository. @@ -2184,9 +2184,9 @@ class GroupProjectManager(ProjectManager): class Group(GitlabObject): _url = '/groups' requiredCreateAttrs = ['name', 'path'] - optionalCreateAttrs = ['description', 'visibility_level', 'parent_id', + optionalCreateAttrs = ['description', 'visibility', 'parent_id', 'lfs_enabled', 'request_access_enabled'] - optionalUpdateAttrs = ['name', 'path', 'description', 'visibility_level', + optionalUpdateAttrs = ['name', 'path', 'description', 'visibility', 'lfs_enabled', 'request_access_enabled'] shortPrintAttr = 'name' managers = ( @@ -2204,10 +2204,6 @@ class Group(GitlabObject): MASTER_ACCESS = gitlab.MASTER_ACCESS OWNER_ACCESS = gitlab.OWNER_ACCESS - VISIBILITY_PRIVATE = gitlab.VISIBILITY_PRIVATE - VISIBILITY_INTERNAL = gitlab.VISIBILITY_INTERNAL - VISIBILITY_PUBLIC = gitlab.VISIBILITY_PUBLIC - def transfer_project(self, id, **kwargs): """Transfers a project to this new groups. From 9a66d780198c5e0abb1abd982063723fe8a16716 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Tue, 23 May 2017 22:44:18 +0200 Subject: [PATCH 22/32] [v4] GroupManager.search is not needed --- gitlab/v4/objects.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index af407f2cb..8b56eeaec 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -2222,16 +2222,3 @@ def transfer_project(self, id, **kwargs): class GroupManager(BaseManager): obj_cls = Group - - def search(self, query, **kwargs): - """Searches groups by name. - - Args: - query (str): The search string - all (bool): If True, return all the items, without pagination - - Returns: - list(Group): a list of matching groups. - """ - url = '/groups?search=' + query - return self.gitlab._raw_list(url, self.obj_cls, **kwargs) From cd18aee5c33315a880d9427a8a201c676e7b3871 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Tue, 23 May 2017 22:47:05 +0200 Subject: [PATCH 23/32] [v4] Rename the ACCESS* variables --- gitlab/v4/objects.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index 8b56eeaec..4d978b0ab 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -34,6 +34,12 @@ VISIBILITY_INTERNAL = 'internal' VISIBILITY_PUBLIC = 'public' +ACCESS_GUEST = 10 +ACCESS_REPORTER = 20 +ACCESS_DEVELOPER = 30 +ACCESS_MASTER = 40 +ACCESS_OWNER = 50 + class SidekiqManager(object): """Manager for the Sidekiq methods. @@ -2198,12 +2204,6 @@ class Group(GitlabObject): ('issues', 'GroupIssueManager', [('group_id', 'id')]), ) - GUEST_ACCESS = gitlab.GUEST_ACCESS - REPORTER_ACCESS = gitlab.REPORTER_ACCESS - DEVELOPER_ACCESS = gitlab.DEVELOPER_ACCESS - MASTER_ACCESS = gitlab.MASTER_ACCESS - OWNER_ACCESS = gitlab.OWNER_ACCESS - def transfer_project(self, id, **kwargs): """Transfers a project to this new groups. From b9eb10a5d090b8357fab72cbc077b45e5d5df115 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Tue, 23 May 2017 22:49:39 +0200 Subject: [PATCH 24/32] 202 is expected on some delete operations --- gitlab/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitlab/__init__.py b/gitlab/__init__.py index d4e7336d6..8beccf0e5 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -495,7 +495,7 @@ def delete(self, obj, id=None, **kwargs): r = self._raw_delete(url, **params) raise_error_from_response(r, GitlabDeleteError, - expected_code=[200, 204]) + expected_code=[200, 202, 204]) return True def create(self, obj, **kwargs): From 449f6071feb626df893f26653d89725dd6fb818b Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Tue, 23 May 2017 22:50:27 +0200 Subject: [PATCH 25/32] [v4] Milestones: iid => iids --- gitlab/v4/objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index 4d978b0ab..876b5727c 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -1414,7 +1414,7 @@ class ProjectMilestone(GitlabObject): _url = '/projects/%(project_id)s/milestones' canDelete = False requiredUrlAttrs = ['project_id'] - optionalListAttrs = ['iid', 'state'] + optionalListAttrs = ['iids', 'state'] requiredCreateAttrs = ['title'] optionalCreateAttrs = ['description', 'due_date', 'start_date', 'state_event'] From 0c3fe39c459d27303e7765c80438e7ade0dda583 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Tue, 23 May 2017 22:55:02 +0200 Subject: [PATCH 26/32] [v4] Update triggers endpoint and attrs --- gitlab/v4/objects.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index 876b5727c..356cb92a8 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -1619,7 +1619,7 @@ class ProjectTrigger(GitlabObject): _url = '/projects/%(project_id)s/triggers' canUpdate = False idAttr = 'token' - requiredUrlAttrs = ['project_id'] + requiredUrlAttrs = ['project_id', 'description'] class ProjectTriggerManager(BaseManager): @@ -2087,7 +2087,7 @@ def share(self, group_id, group_access, **kwargs): r = self.gitlab._raw_post(url, data=data, **kwargs) raise_error_from_response(r, GitlabCreateError, 201) - def trigger_build(self, ref, token, variables={}, **kwargs): + def trigger_pipeline(self, ref, token, variables={}, **kwargs): """Trigger a CI build. See https://gitlab.com/help/ci/triggers/README.md#trigger-a-build @@ -2101,7 +2101,7 @@ def trigger_build(self, ref, token, variables={}, **kwargs): GitlabConnectionError: If the server cannot be reached. GitlabCreateError: If the server fails to perform the request. """ - url = "/projects/%s/trigger/builds" % self.id + url = "/projects/%s/trigger/pipeline" % self.id form = {r'variables[%s]' % k: v for k, v in six.iteritems(variables)} data = {'ref': ref, 'token': token} data.update(form) From 0d1ace10f160f69ed7f20d5ddaa229361641e4d9 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Tue, 23 May 2017 23:19:58 +0200 Subject: [PATCH 27/32] [v4] Try to make the files raw() method work --- gitlab/v4/objects.py | 58 ++++++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index 356cb92a8..54369d9d5 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -1498,13 +1498,12 @@ class ProjectFile(GitlabObject): _url = '/projects/%(project_id)s/repository/files' canList = False requiredUrlAttrs = ['project_id'] - requiredGetAttrs = ['file_path', 'ref'] + requiredGetAttrs = ['ref'] requiredCreateAttrs = ['file_path', 'branch', 'content', 'commit_message'] optionalCreateAttrs = ['encoding'] requiredDeleteAttrs = ['branch', 'commit_message', 'file_path'] shortPrintAttr = 'file_path' - getRequiresId = False def decode(self): """Returns the decoded content of the file. @@ -1518,6 +1517,34 @@ def decode(self): class ProjectFileManager(BaseManager): obj_cls = ProjectFile + def raw(self, filepath, ref, streamed=False, action=None, chunk_size=1024, + **kwargs): + """Return the content of a file for a commit. + + Args: + ref (str): ID of the commit + filepath (str): Path of the file to return + streamed (bool): If True the data will be processed by chunks of + `chunk_size` and each chunk is passed to `action` for + treatment. + action (callable): Callable responsible of dealing with chunk of + data. + chunk_size (int): Size of each chunk. + + Returns: + str: The file content + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabGetError: If the server fails to perform the request. + """ + url = ("/projects/%s/repository/files/%s/raw" % + (self.parent.id, filepath.replace('/', '%2F'))) + url += '?ref=%s' % ref + r = self.gitlab._raw_get(url, streamed=streamed, **kwargs) + raise_error_from_response(r, GitlabGetError) + return utils.response_content(r, streamed, action, chunk_size) + class ProjectPipeline(GitlabObject): _url = '/projects/%(project_id)s/pipelines' @@ -1861,33 +1888,6 @@ def repository_tree(self, path='', ref_name='', **kwargs): raise_error_from_response(r, GitlabGetError) return r.json() - def repository_blob(self, sha, filepath, streamed=False, action=None, - chunk_size=1024, **kwargs): - """Return the content of a file for a commit. - - Args: - sha (str): ID of the commit - filepath (str): Path of the file to return - streamed (bool): If True the data will be processed by chunks of - `chunk_size` and each chunk is passed to `action` for - treatment. - action (callable): Callable responsible of dealing with chunk of - data. - chunk_size (int): Size of each chunk. - - Returns: - str: The file content - - Raises: - GitlabConnectionError: If the server cannot be reached. - GitlabGetError: If the server fails to perform the request. - """ - url = "/projects/%s/repository/blobs/%s" % (self.id, sha) - url += '?%s' % (urllib.urlencode({'filepath': filepath})) - r = self.gitlab._raw_get(url, streamed=streamed, **kwargs) - raise_error_from_response(r, GitlabGetError) - return utils.response_content(r, streamed, action, chunk_size) - def repository_raw_blob(self, sha, streamed=False, action=None, chunk_size=1024, **kwargs): """Returns the raw file contents for a blob by blob SHA. From 2dd84e8170502ded3fb8f9b62e0571351ad6e0be Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Tue, 23 May 2017 23:20:46 +0200 Subject: [PATCH 28/32] [v4] repository tree: s/ref_name/ref/ --- gitlab/v4/objects.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index 54369d9d5..694404680 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -1862,12 +1862,12 @@ class Project(GitlabObject): ('variables', 'ProjectVariableManager', [('project_id', 'id')]), ) - def repository_tree(self, path='', ref_name='', **kwargs): + def repository_tree(self, path='', ref='', **kwargs): """Return a list of files in the repository. Args: path (str): Path of the top folder (/ by default) - ref_name (str): Reference to a commit or branch + ref (str): Reference to a commit or branch Returns: str: The json representation of the tree. @@ -1880,8 +1880,8 @@ def repository_tree(self, path='', ref_name='', **kwargs): params = [] if path: params.append(urllib.urlencode({'path': path})) - if ref_name: - params.append("ref_name=%s" % ref_name) + if ref: + params.append("ref=%s" % ref) if params: url += '?' + "&".join(params) r = self.gitlab._raw_get(url, **kwargs) From cd98903d6c1a2cbf21d533d6d6d4ea58917930b1 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Tue, 23 May 2017 23:21:21 +0200 Subject: [PATCH 29/32] [v4] Users confirm attribute renamed skip_confirmation --- gitlab/v4/objects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index 694404680..0a6ae41dc 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -128,13 +128,13 @@ class User(GitlabObject): optionalCreateAttrs = ['password', 'reset_password', 'skype', 'linkedin', 'twitter', 'projects_limit', 'extern_uid', 'provider', 'bio', 'admin', 'can_create_group', - 'website_url', 'confirm', 'external', + 'website_url', 'skip_confirmation', 'external', 'organization', 'location'] requiredUpdateAttrs = ['email', 'username', 'name'] optionalUpdateAttrs = ['password', 'skype', 'linkedin', 'twitter', 'projects_limit', 'extern_uid', 'provider', 'bio', 'admin', 'can_create_group', 'website_url', - 'confirm', 'external', 'organization', 'location'] + 'skip_confirmation', 'external', 'organization', 'location'] managers = ( ('emails', 'UserEmailManager', [('user_id', 'id')]), ('keys', 'UserKeyManager', [('user_id', 'id')]), From 441244b8d91ac0674195dbb2151570712d234d15 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Tue, 23 May 2017 23:22:24 +0200 Subject: [PATCH 30/32] pop8 fixes --- gitlab/v4/objects.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index 0a6ae41dc..4e6589383 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -134,7 +134,8 @@ class User(GitlabObject): optionalUpdateAttrs = ['password', 'skype', 'linkedin', 'twitter', 'projects_limit', 'extern_uid', 'provider', 'bio', 'admin', 'can_create_group', 'website_url', - 'skip_confirmation', 'external', 'organization', 'location'] + 'skip_confirmation', 'external', 'organization', + 'location'] managers = ( ('emails', 'UserEmailManager', [('user_id', 'id')]), ('keys', 'UserKeyManager', [('user_id', 'id')]), @@ -1236,7 +1237,8 @@ def unsubscribe(self, **kwargs): def cancel_merge_when_pipeline_succeeds(self, **kwargs): """Cancel merge when build succeeds.""" - u = ('/projects/%s/merge_requests/%s/cancel_merge_when_pipeline_succeeds' + u = ('/projects/%s/merge_requests/%s/' + 'cancel_merge_when_pipeline_succeeds' % (self.project_id, self.id)) r = self.gitlab._raw_put(u, **kwargs) errors = {401: GitlabMRForbiddenError, From 8e4b65fc78f47a2be658b11ae30f84da66b13c2a Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Wed, 24 May 2017 07:34:00 +0200 Subject: [PATCH 31/32] [v4] Remove deprecated objects methods and classes --- gitlab/__init__.py | 4 ++-- gitlab/v4/objects.py | 28 ---------------------------- 2 files changed, 2 insertions(+), 30 deletions(-) diff --git a/gitlab/__init__.py b/gitlab/__init__.py index 8beccf0e5..d4ea1d914 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -96,7 +96,6 @@ def __init__(self, url, private_token=None, email=None, password=None, self._api_version) self.broadcastmessages = objects.BroadcastMessageManager(self) - self.keys = objects.KeyManager(self) self.deploykeys = objects.DeployKeyManager(self) self.gitlabciymls = objects.GitlabciymlManager(self) self.gitignores = objects.GitignoreManager(self) @@ -112,9 +111,10 @@ def __init__(self, url, private_token=None, email=None, password=None, self.sidekiq = objects.SidekiqManager(self) self.snippets = objects.SnippetManager(self) self.users = objects.UserManager(self) + self.todos = objects.TodoManager(self) if self._api_version == '3': + self.keys = objects.KeyManager(self) self.teams = objects.TeamManager(self) - self.todos = objects.TodoManager(self) # build the "submanagers" for parent_cls in six.itervalues(vars(objects)): diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index 4e6589383..eb3a5799c 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -307,23 +307,6 @@ class BroadcastMessageManager(BaseManager): obj_cls = BroadcastMessage -class Key(GitlabObject): - _url = '/deploy_keys' - canGet = 'from_list' - canCreate = False - canUpdate = False - canDelete = False - - def __init__(self, *args, **kwargs): - warnings.warn("`Key` is deprecated, use `DeployKey` instead", - DeprecationWarning) - super(Key, self).__init__(*args, **kwargs) - - -class KeyManager(BaseManager): - obj_cls = Key - - class DeployKey(GitlabObject): _url = '/deploy_keys' canGet = 'from_list' @@ -2047,11 +2030,6 @@ def archive(self, **kwargs): raise_error_from_response(r, GitlabCreateError, 201) return Project(self.gitlab, r.json()) if r.status_code == 201 else self - def archive_(self, **kwargs): - warnings.warn("`archive_()` is deprecated, use `archive()` instead", - DeprecationWarning) - return self.archive(**kwargs) - def unarchive(self, **kwargs): """Unarchive a project. @@ -2067,12 +2045,6 @@ def unarchive(self, **kwargs): raise_error_from_response(r, GitlabCreateError, 201) return Project(self.gitlab, r.json()) if r.status_code == 201 else self - def unarchive_(self, **kwargs): - warnings.warn("`unarchive_()` is deprecated, " - "use `unarchive()` instead", - DeprecationWarning) - return self.unarchive(**kwargs) - def share(self, group_id, group_access, **kwargs): """Share the project with a group. From dcbb5015626190528a160b4bf93ba18e72c48fff Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Wed, 24 May 2017 07:49:53 +0200 Subject: [PATCH 32/32] [v4] User: drop the manager filters --- gitlab/v4/objects.py | 42 ++---------------------------------------- 1 file changed, 2 insertions(+), 40 deletions(-) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index eb3a5799c..6e6c7591c 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -124,6 +124,8 @@ class UserProjectManager(BaseManager): class User(GitlabObject): _url = '/users' shortPrintAttr = 'username' + optionalListAttrs = ['active', 'blocked', 'username', 'extern_uid', + 'provider', 'external'] requiredCreateAttrs = ['email', 'username', 'name'] optionalCreateAttrs = ['password', 'reset_password', 'skype', 'linkedin', 'twitter', 'projects_limit', 'extern_uid', @@ -175,46 +177,6 @@ def __eq__(self, other): class UserManager(BaseManager): obj_cls = User - def search(self, query, **kwargs): - """Search users. - - Args: - query (str): The query string to send to GitLab for the search. - all (bool): If True, return all the items, without pagination - **kwargs: Additional arguments to send to GitLab. - - Returns: - list(User): A list of matching users. - - Raises: - GitlabConnectionError: If the server cannot be reached. - GitlabListError: If the server fails to perform the request. - """ - url = self.obj_cls._url + '?search=' + query - return self.gitlab._raw_list(url, self.obj_cls, **kwargs) - - def get_by_username(self, username, **kwargs): - """Get a user by its username. - - Args: - username (str): The name of the user. - **kwargs: Additional arguments to send to GitLab. - - Returns: - User: The matching user. - - Raises: - GitlabConnectionError: If the server cannot be reached. - GitlabGetError: If the server fails to perform the request. - """ - url = self.obj_cls._url + '?username=' + username - results = self.gitlab._raw_list(url, self.obj_cls, **kwargs) - assert len(results) in (0, 1) - try: - return results[0] - except IndexError: - raise GitlabGetError('no such user: ' + username) - class CurrentUserEmail(GitlabObject): _url = '/user/emails'