summaryrefslogtreecommitdiff
path: root/gitlab
diff options
context:
space:
mode:
authorGauvain Pocentek <gauvain@pocentek.net>2017-05-24 07:58:42 +0200
committerGauvain Pocentek <gauvain@pocentek.net>2017-05-24 07:58:42 +0200
commit766efe6180041f9730d2966549637754bb85d868 (patch)
treee31ca6a5fc0c1179c34a8bc78a1e93fbf015bf07 /gitlab
parent7ac1e4c1fe4ccff8c8ee4a9ae212a227d5499bce (diff)
parentdcbb5015626190528a160b4bf93ba18e72c48fff (diff)
downloadgitlab-766efe6180041f9730d2966549637754bb85d868.tar.gz
Merge branch 'v4_support'
Diffstat (limited to 'gitlab')
-rw-r--r--gitlab/__init__.py72
-rw-r--r--gitlab/base.py533
-rw-r--r--gitlab/config.py9
-rw-r--r--gitlab/tests/test_manager.py2
-rw-r--r--gitlab/v3/__init__.py0
-rw-r--r--gitlab/v3/objects.py (renamed from gitlab/objects.py)604
-rw-r--r--gitlab/v4/__init__.py0
-rw-r--r--gitlab/v4/objects.py2160
8 files changed, 2800 insertions, 580 deletions
diff --git a/gitlab/__init__.py b/gitlab/__init__.py
index 7ea0a6a..63766ff 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 six
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'
@@ -65,13 +66,15 @@ 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,
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
@@ -89,42 +92,50 @@ class Gitlab(object):
#: 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.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.todos = objects.TodoManager(self)
+ if self._api_version == '3':
+ self.keys = objects.KeyManager(self)
+ self.teams = objects.TeamManager(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
+ def api_version(self):
+ return self._api_version
+
def _cls_to_manager_prefix(self, cls):
# Manage bad naming decisions
camel_case = (cls.__name__
@@ -152,7 +163,8 @@ class Gitlab(object):
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.
@@ -225,7 +237,7 @@ class Gitlab(object):
warnings.warn('set_url() is deprecated, create a new Gitlab instance '
'if you need an updated URL.',
DeprecationWarning)
- 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:
@@ -505,7 +517,7 @@ class Gitlab(object):
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):
diff --git a/gitlab/base.py b/gitlab/base.py
new file mode 100644
index 0000000..aa660b2
--- /dev/null
+++ b/gitlab/base.py
@@ -0,0 +1,533 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2013-2017 Gauvain Pocentek <gauvain@pocentek.net>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+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)
diff --git a/gitlab/config.py b/gitlab/config.py
index 3ef2efb..9af804d 100644
--- a/gitlab/config.py
+++ b/gitlab/config.py
@@ -88,3 +88,12 @@ class GitlabConfigParser(object):
'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)
diff --git a/gitlab/tests/test_manager.py b/gitlab/tests/test_manager.py
index 16e13f2..4f4dbe1 100644
--- a/gitlab/tests/test_manager.py
+++ b/gitlab/tests/test_manager.py
@@ -25,7 +25,7 @@ from httmock import response # noqa
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 0000000..e69de29
--- /dev/null
+++ b/gitlab/v3/__init__.py
diff --git a/gitlab/objects.py b/gitlab/v3/objects.py
index 0def183..01bb670 100644
--- a/gitlab/objects.py
+++ b/gitlab/v3/objects.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
-# Copyright (C) 2013-2015 Gauvain Pocentek <gauvain@pocentek.net>
+# Copyright (C) 2013-2017 Gauvain Pocentek <gauvain@pocentek.net>
#
# 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 print_function
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')]),
)
@@ -2278,6 +1777,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',
@@ -2298,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
@@ -2678,6 +2179,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 +2199,9 @@ class ProjectManager(BaseManager):
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)
@@ -2762,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
@@ -2836,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 0000000..e69de29
--- /dev/null
+++ b/gitlab/v4/__init__.py
diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py
new file mode 100644
index 0000000..6e6c759
--- /dev/null
+++ b/gitlab/v4/objects.py
@@ -0,0 +1,2160 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2013-2017 Gauvain Pocentek <gauvain@pocentek.net>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+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
+
+VISIBILITY_PRIVATE = 'private'
+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.
+
+ 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',
+ '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'
+ optionalListAttrs = ['active', 'blocked', 'username', 'extern_uid',
+ 'provider', 'external']
+ requiredCreateAttrs = ['email', 'username', 'name']
+ optionalCreateAttrs = ['password', 'reset_password', 'skype', 'linkedin',
+ 'twitter', 'projects_limit', 'extern_uid',
+ 'provider', 'bio', 'admin', 'can_create_group',
+ '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',
+ 'skip_confirmation', '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_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_post(url, **kwargs)
+ raise_error_from_response(r, GitlabUnblockError, 201)
+ 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
+
+
+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 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 = '/templates/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']
+ optionalUpdateAttrs = ['title', 'file_name', 'content', 'visibility']
+ 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', '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', '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/deploy_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)
+
+
+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/%(project_id)s/fork'
+ 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', '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/subscribe' %
+ {'project_id': self.project_id, 'issue_id': self.id})
+
+ 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 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/unsubscribe' %
+ {'project_id': self.project_id, 'issue_id': self.id})
+
+ 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):
+ """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 = ['iids', '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/'
+ 'subscribe' %
+ {'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/'
+ 'unsubscribe' %
+ {'project_id': self.project_id, 'mr_id': self.id})
+
+ 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())
+
+ def cancel_merge_when_pipeline_succeeds(self, **kwargs):
+ """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,
+ 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 = ['iids', '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/subscribe' %
+ {'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/unsubscribe' %
+ {'project_id': self.project_id, 'label_id': self.name})
+
+ r = self.gitlab._raw_post(url, **kwargs)
+ raise_error_from_response(r, GitlabUnsubscribeError, [201, 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 = ['ref']
+ requiredCreateAttrs = ['file_path', 'branch', 'content',
+ 'commit_message']
+ optionalCreateAttrs = ['encoding']
+ requiredDeleteAttrs = ['branch', 'commit_message', 'file_path']
+ shortPrintAttr = 'file_path'
+
+ 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
+
+ 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'
+ _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']
+ optionalUpdateAttrs = ['title', 'file_name', 'code', 'visibility']
+ 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', 'description']
+
+
+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', 'owned', 'starred', 'archived',
+ 'visibility', 'order_by', 'sort', 'simple',
+ 'membership', 'statistics']
+ optionalCreateAttrs = ['path', 'namespace_id', 'description',
+ 'issues_enabled', 'merge_requests_enabled',
+ 'builds_enabled', 'wiki_enabled',
+ 'snippets_enabled', 'container_registry_enabled',
+ 'shared_runners_enabled', 'visibility',
+ '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', 'visibility',
+ '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')]),
+ )
+
+ 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 (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:
+ params.append("ref=%s" % ref)
+ if params:
+ url += '?' + "&".join(params)
+ r = self.gitlab._raw_get(url, **kwargs)
+ raise_error_from_response(r, GitlabGetError)
+ return r.json()
+
+ 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/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.
+
+ 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 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 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_pipeline(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/pipeline" % 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 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
+
+
+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', 'parent_id',
+ 'lfs_enabled', 'request_access_enabled']
+ optionalUpdateAttrs = ['name', 'path', 'description', 'visibility',
+ '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')]),
+ )
+
+ 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