summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGauvain Pocentek <gauvain@pocentek.net>2017-08-04 18:45:16 +0200
committerGauvain Pocentek <gauvain@pocentek.net>2017-08-04 18:45:16 +0200
commit3ccdec04525456c906f26ee2e931607a5d0dcd20 (patch)
tree48709c487d57c738eb881a2728a3300023c482e5
parente87835fe02aeb174c1b0355a1733733d89b2e404 (diff)
parent2816c1ae51b01214012679b74aa14de1a6696eb5 (diff)
downloadgitlab-3ccdec04525456c906f26ee2e931607a5d0dcd20.tar.gz
Merge branch 'rework_api'
-rw-r--r--.travis.yml1
-rw-r--r--MANIFEST.in2
-rw-r--r--docs/api/gitlab.rst78
-rw-r--r--docs/api/gitlab.v3.rst22
-rw-r--r--docs/api/gitlab.v4.rst22
-rw-r--r--docs/api/modules.rst7
-rw-r--r--docs/ext/docstrings.py14
-rw-r--r--docs/index.rst4
-rw-r--r--docs/switching-to-v4.rst116
-rw-r--r--docs/upgrade-from-0.10.rst125
-rw-r--r--gitlab/__init__.py325
-rw-r--r--gitlab/base.py163
-rw-r--r--gitlab/cli.py522
-rw-r--r--gitlab/exceptions.py28
-rw-r--r--gitlab/mixins.py438
-rw-r--r--gitlab/tests/test_base.py129
-rw-r--r--gitlab/tests/test_cli.py37
-rw-r--r--gitlab/tests/test_gitlab.py271
-rw-r--r--gitlab/tests/test_gitlabobject.py1
-rw-r--r--gitlab/tests/test_mixins.py411
-rw-r--r--gitlab/v3/cli.py497
-rw-r--r--gitlab/v3/objects.py1
-rw-r--r--gitlab/v4/objects.py3012
-rwxr-xr-xtools/build_test_env.sh2
-rwxr-xr-xtools/py_functional_tests.sh2
-rw-r--r--tools/python_test_v3.py (renamed from tools/python_test.py)10
-rw-r--r--tools/python_test_v4.py341
27 files changed, 4359 insertions, 2222 deletions
diff --git a/.travis.yml b/.travis.yml
index 7c8b9fd..dd405f5 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -10,7 +10,6 @@ addons:
language: python
python: 2.7
env:
- - TOX_ENV=py36
- TOX_ENV=py35
- TOX_ENV=py34
- TOX_ENV=py27
diff --git a/MANIFEST.in b/MANIFEST.in
index e677be7..3cc3cdc 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,4 +1,4 @@
-include COPYING AUTHORS ChangeLog requirements.txt test-requirements.txt rtd-requirements.txt
+include COPYING AUTHORS ChangeLog.rst RELEASE_NOTES.rst requirements.txt test-requirements.txt rtd-requirements.txt
include tox.ini .testr.conf .travis.yml
recursive-include tools *
recursive-include docs *j2 *.py *.rst api/*.rst Makefile make.bat
diff --git a/docs/api/gitlab.rst b/docs/api/gitlab.rst
index d34d56f..e75f843 100644
--- a/docs/api/gitlab.rst
+++ b/docs/api/gitlab.rst
@@ -1,55 +1,48 @@
gitlab package
==============
-Module contents
----------------
+Subpackages
+-----------
-.. automodule:: gitlab
+.. toctree::
+
+ gitlab.v3
+ gitlab.v4
+
+Submodules
+----------
+
+gitlab.base module
+------------------
+
+.. automodule:: gitlab.base
:members:
:undoc-members:
:show-inheritance:
- :exclude-members: Hook, UserProject, Group, Issue, Team, User,
- all_projects, owned_projects, search_projects
-gitlab.base
------------
+gitlab.cli module
+-----------------
-.. automodule:: gitlab.base
+.. automodule:: gitlab.cli
:members:
:undoc-members:
:show-inheritance:
-gitlab.v3.objects module
-------------------------
+gitlab.config module
+--------------------
-.. automodule:: gitlab.v3.objects
+.. automodule:: gitlab.config
:members:
:undoc-members:
:show-inheritance:
- :exclude-members: Branch, Commit, Content, Event, File, Hook, Issue, Key,
- Label, Member, MergeRequest, Milestone, Note, Snippet,
- Tag, canGet, canList, canUpdate, canCreate, canDelete,
- requiredUrlAttrs, requiredListAttrs, optionalListAttrs,
- optionalGetAttrs, requiredGetAttrs, requiredDeleteAttrs,
- requiredCreateAttrs, optionalCreateAttrs,
- requiredUpdateAttrs, optionalUpdateAttrs, getRequiresId,
- shortPrintAttr, idAttr
-gitlab.v4.objects module
-------------------------
+gitlab.const module
+-------------------
-.. automodule:: gitlab.v4.objects
+.. automodule:: gitlab.const
:members:
:undoc-members:
:show-inheritance:
- :exclude-members: Branch, Commit, Content, Event, File, Hook, Issue, Key,
- Label, Member, MergeRequest, Milestone, Note, Snippet,
- Tag, canGet, canList, canUpdate, canCreate, canDelete,
- requiredUrlAttrs, requiredListAttrs, optionalListAttrs,
- optionalGetAttrs, requiredGetAttrs, requiredDeleteAttrs,
- requiredCreateAttrs, optionalCreateAttrs,
- requiredUpdateAttrs, optionalUpdateAttrs, getRequiresId,
- shortPrintAttr, idAttr
gitlab.exceptions module
------------------------
@@ -58,3 +51,28 @@ gitlab.exceptions module
:members:
:undoc-members:
:show-inheritance:
+
+gitlab.mixins module
+--------------------
+
+.. automodule:: gitlab.mixins
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+gitlab.utils module
+-------------------
+
+.. automodule:: gitlab.utils
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+
+Module contents
+---------------
+
+.. automodule:: gitlab
+ :members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/docs/api/gitlab.v3.rst b/docs/api/gitlab.v3.rst
new file mode 100644
index 0000000..61879bc
--- /dev/null
+++ b/docs/api/gitlab.v3.rst
@@ -0,0 +1,22 @@
+gitlab.v3 package
+=================
+
+Submodules
+----------
+
+gitlab.v3.objects module
+------------------------
+
+.. automodule:: gitlab.v3.objects
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+
+Module contents
+---------------
+
+.. automodule:: gitlab.v3
+ :members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/docs/api/gitlab.v4.rst b/docs/api/gitlab.v4.rst
new file mode 100644
index 0000000..70358c1
--- /dev/null
+++ b/docs/api/gitlab.v4.rst
@@ -0,0 +1,22 @@
+gitlab.v4 package
+=================
+
+Submodules
+----------
+
+gitlab.v4.objects module
+------------------------
+
+.. automodule:: gitlab.v4.objects
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+
+Module contents
+---------------
+
+.. automodule:: gitlab.v4
+ :members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/docs/api/modules.rst b/docs/api/modules.rst
deleted file mode 100644
index 3ec5a68..0000000
--- a/docs/api/modules.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-API documentation
-=================
-
-.. toctree::
- :maxdepth: 4
-
- gitlab
diff --git a/docs/ext/docstrings.py b/docs/ext/docstrings.py
index fc95eeb..32c5da1 100644
--- a/docs/ext/docstrings.py
+++ b/docs/ext/docstrings.py
@@ -10,6 +10,8 @@ from sphinx.ext.napoleon.docstring import GoogleDocstring
def classref(value, short=True):
+ return value
+
if not inspect.isclass(value):
return ':class:%s' % value
tilde = '~' if short else ''
@@ -46,8 +48,13 @@ class GitlabDocstring(GoogleDocstring):
return output.split('\n')
- def __init__(self, *args, **kwargs):
- super(GitlabDocstring, self).__init__(*args, **kwargs)
+ def __init__(self, docstring, config=None, app=None, what='', name='',
+ obj=None, options=None):
+ super(GitlabDocstring, self).__init__(docstring, config, app, what,
+ name, obj, options)
+
+ if name and name.startswith('gitlab.v4.objects'):
+ return
if getattr(self._obj, '__name__', None) == 'Gitlab':
mgrs = []
@@ -57,9 +64,12 @@ class GitlabDocstring(GoogleDocstring):
mgrs.append(item)
self._parsed_lines.extend(self._build_doc('gl_tmpl.j2',
mgrs=sorted(mgrs)))
+
+ # BaseManager
elif hasattr(self._obj, 'obj_cls') and self._obj.obj_cls is not None:
self._parsed_lines.extend(self._build_doc('manager_tmpl.j2',
cls=self._obj.obj_cls))
+ # GitlabObject
elif hasattr(self._obj, 'canUpdate') and self._obj.canUpdate:
self._parsed_lines.extend(self._build_doc('object_tmpl.j2',
obj=self._obj))
diff --git a/docs/index.rst b/docs/index.rst
index 2198025..7805fcf 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -14,9 +14,9 @@ Contents:
install
cli
api-usage
+ switching-to-v4
api-objects
- upgrade-from-0.10
- api/modules
+ api/gitlab
release_notes
changelog
diff --git a/docs/switching-to-v4.rst b/docs/switching-to-v4.rst
new file mode 100644
index 0000000..84181ff
--- /dev/null
+++ b/docs/switching-to-v4.rst
@@ -0,0 +1,116 @@
+##########################
+Switching to GtiLab API v4
+##########################
+
+GitLab provides a new API version (v4) since its 9.0 release. ``python-gitlab``
+provides support for this new version, but the python API has been modified to
+solve some problems with the existing one.
+
+GitLab will stop supporting the v3 API soon, and you should consider switching
+to v4 if you use a recent version of GitLab (>= 9.0), or if you use
+http://gitlab.com.
+
+The new v4 API is available in the `rework_api branch on github
+<https://github.com/python-gitlab/python-gitlab/tree/rework_api>`_, and will be
+released soon.
+
+
+Using the v4 API
+================
+
+To use the new v4 API, explicitly use it in the ``Gitlab`` constructor:
+
+.. code-block:: python
+
+ gl = gitlab.Gitlab(..., api_version=4)
+
+
+If you use the configuration file, also explicitly define the version:
+
+.. code-block:: ini
+
+ [my_gitlab]
+ ...
+ api_version = 4
+
+
+Changes between v3 and v4 API
+=============================
+
+For a list of GtiLab (upstream) API changes, see
+https://docs.gitlab.com/ce/api/v3_to_v4.html.
+
+The ``python-gitlab`` API reflects these changes. But also consider the
+following important changes in the python API:
+
+* managers and objects don't inherit from ``GitlabObject`` and ``BaseManager``
+ anymore. They inherit from :class:`~gitlab.base.RESTManager` and
+ :class:`~gitlab.base.RESTObject`.
+
+* You should only use the managers to perform CRUD operations.
+
+ The following v3 code:
+
+ .. code-block:: python
+
+ gl = gitlab.Gitlab(...)
+ p = Project(gl, project_id)
+
+ Should be replaced with:
+
+ .. code-block:: python
+
+ gl = gitlab.Gitlab(...)
+ p = gl.projects.get(project_id)
+
+* Listing methods (``manager.list()`` for instance) can now return generators
+ (:class:`~gitlab.base.RESTObjectList`). They handle the calls to the API when
+ needed to fetch new items.
+
+ By default you will still get lists. To get generators use ``as_list=False``:
+
+ .. code-block:: python
+
+ all_projects_g = gl.projects.list(as_list=False)
+
+* The "nested" managers (for instance ``gl.project_issues`` or
+ ``gl.group_members``) are not available anymore. Their goal was to provide a
+ direct way to manage nested objects, and to limit the number of needed API
+ calls.
+
+ To limit the number of API calls, you can now use ``get()`` methods with the
+ ``lazy=True`` parameter. This creates shallow objects that provide usual
+ managers.
+
+ The following v3 code:
+
+ .. code-block:: python
+
+ issues = gl.project_issues.list(project_id=project_id)
+
+ Should be replaced with:
+
+ .. code-block:: python
+
+ issues = gl.projects.get(project_id, lazy=True).issues.list()
+
+ This will make only one API call, instead of two if ``lazy`` is not used.
+
+* The :class:`~gitlab.Gitlab` folowwing methods should not be used anymore for
+ v4:
+
+ + ``list()``
+ + ``get()``
+ + ``create()``
+ + ``update()``
+ + ``delete()``
+
+* If you need to perform HTTP requests to the GitLab server (which you
+ shouldn't), you can use the following :class:`~gitlab.Gitlab` methods:
+
+ + :attr:`~gitlab.Gitlab.http_request`
+ + :attr:`~gitlab.Gitlab.http_get`
+ + :attr:`~gitlab.Gitlab.http_list`
+ + :attr:`~gitlab.Gitlab.http_post`
+ + :attr:`~gitlab.Gitlab.http_put`
+ + :attr:`~gitlab.Gitlab.http_delete`
diff --git a/docs/upgrade-from-0.10.rst b/docs/upgrade-from-0.10.rst
deleted file mode 100644
index 7ff80ab..0000000
--- a/docs/upgrade-from-0.10.rst
+++ /dev/null
@@ -1,125 +0,0 @@
-#############################################
-Upgrading from python-gitlab 0.10 and earlier
-#############################################
-
-``python-gitlab`` 0.11 introduces new objects which make the API cleaner and
-easier to use. The feature set is unchanged but some methods have been
-deprecated in favor of the new manager objects.
-
-Deprecated methods will be remove in a future release.
-
-Gitlab object migration
-=======================
-
-The objects constructor methods are deprecated:
-
-* ``Hook()``
-* ``Project()``
-* ``UserProject()``
-* ``Group()``
-* ``Issue()``
-* ``User()``
-* ``Team()``
-
-Use the new managers objects instead. For example:
-
-.. code-block:: python
-
- # Deprecated syntax
- p1 = gl.Project({'name': 'myCoolProject'})
- p1.save()
- p2 = gl.Project(id=1)
- p_list = gl.Project()
-
- # New syntax
- p1 = gl.projects.create({'name': 'myCoolProject'})
- p2 = gl.projects.get(1)
- p_list = gl.projects.list()
-
-The following methods are also deprecated:
-
-* ``search_projects()``
-* ``owned_projects()``
-* ``all_projects()``
-
-Use the ``projects`` manager instead:
-
-.. code-block:: python
-
- # Deprecated syntax
- l1 = gl.search_projects('whatever')
- l2 = gl.owned_projects()
- l3 = gl.all_projects()
-
- # New syntax
- l1 = gl.projects.search('whatever')
- l2 = gl.projects.owned()
- l3 = gl.projects.all()
-
-GitlabObject objects migration
-==============================
-
-The following constructor methods are deprecated in favor of the matching
-managers:
-
-.. list-table::
- :header-rows: 1
-
- * - Deprecated method
- - Matching manager
- * - ``User.Key()``
- - ``User.keys``
- * - ``CurrentUser.Key()``
- - ``CurrentUser.keys``
- * - ``Group.Member()``
- - ``Group.members``
- * - ``ProjectIssue.Note()``
- - ``ProjectIssue.notes``
- * - ``ProjectMergeRequest.Note()``
- - ``ProjectMergeRequest.notes``
- * - ``ProjectSnippet.Note()``
- - ``ProjectSnippet.notes``
- * - ``Project.Branch()``
- - ``Project.branches``
- * - ``Project.Commit()``
- - ``Project.commits``
- * - ``Project.Event()``
- - ``Project.events``
- * - ``Project.File()``
- - ``Project.files``
- * - ``Project.Hook()``
- - ``Project.hooks``
- * - ``Project.Key()``
- - ``Project.keys``
- * - ``Project.Issue()``
- - ``Project.issues``
- * - ``Project.Label()``
- - ``Project.labels``
- * - ``Project.Member()``
- - ``Project.members``
- * - ``Project.MergeRequest()``
- - ``Project.mergerequests``
- * - ``Project.Milestone()``
- - ``Project.milestones``
- * - ``Project.Note()``
- - ``Project.notes``
- * - ``Project.Snippet()``
- - ``Project.snippets``
- * - ``Project.Tag()``
- - ``Project.tags``
- * - ``Team.Member()``
- - ``Team.members``
- * - ``Team.Project()``
- - ``Team.projects``
-
-For example:
-
-.. code-block:: python
-
- # Deprecated syntax
- p = gl.Project(id=2)
- issues = p.Issue()
-
- # New syntax
- p = gl.projects.get(2)
- issues = p.issues.list()
diff --git a/gitlab/__init__.py b/gitlab/__init__.py
index b419cb8..bdeb5c4 100644
--- a/gitlab/__init__.py
+++ b/gitlab/__init__.py
@@ -17,7 +17,6 @@
"""Wrapper for the GitLab API."""
from __future__ import print_function
-from __future__ import division
from __future__ import absolute_import
import importlib
import inspect
@@ -94,6 +93,7 @@ class Gitlab(object):
objects = importlib.import_module('gitlab.v%s.objects' %
self._api_version)
+ self._objects = objects
self.broadcastmessages = objects.BroadcastMessageManager(self)
self.deploykeys = objects.DeployKeyManager(self)
@@ -118,21 +118,22 @@ class Gitlab(object):
else:
self.dockerfiles = objects.DockerfileManager(self)
- # build the "submanagers"
- for parent_cls in six.itervalues(vars(objects)):
- if (not inspect.isclass(parent_cls)
- or not issubclass(parent_cls, objects.GitlabObject)
- or parent_cls == objects.CurrentUser):
- continue
-
- if not parent_cls.managers:
- continue
-
- for var, cls_name, attrs in parent_cls.managers:
- var_name = '%s_%s' % (self._cls_to_manager_prefix(parent_cls),
- var)
- manager = getattr(objects, cls_name)(self)
- setattr(self, var_name, manager)
+ if self._api_version == '3':
+ # build the "submanagers"
+ for parent_cls in six.itervalues(vars(objects)):
+ if (not inspect.isclass(parent_cls)
+ or not issubclass(parent_cls, objects.GitlabObject)
+ or parent_cls == objects.CurrentUser):
+ continue
+
+ if not parent_cls.managers:
+ continue
+
+ for var, cls_name, attrs in parent_cls.managers:
+ prefix = self._cls_to_manager_prefix(parent_cls)
+ var_name = '%s_%s' % (prefix, var)
+ manager = getattr(objects, cls_name)(self)
+ setattr(self, var_name, manager)
@property
def api_version(self):
@@ -191,13 +192,16 @@ class Gitlab(object):
if not self.email or not self.password:
raise GitlabAuthenticationError("Missing email/password")
- data = json.dumps({'email': self.email, 'password': self.password})
- r = self._raw_post('/session', data, content_type='application/json')
- raise_error_from_response(r, GitlabAuthenticationError, 201)
- self.user = CurrentUser(self, r.json())
- """(gitlab.objects.CurrentUser): Object representing the user currently
- logged.
- """
+ if self.api_version == '3':
+ data = json.dumps({'email': self.email, 'password': self.password})
+ r = self._raw_post('/session', data,
+ content_type='application/json')
+ raise_error_from_response(r, GitlabAuthenticationError, 201)
+ self.user = self._objects.CurrentUser(self, r.json())
+ else:
+ manager = self._objects.CurrentUserManager()
+ self.user = manager.get(self.email, self.password)
+
self._set_token(self.user.private_token)
def token_auth(self):
@@ -207,7 +211,10 @@ class Gitlab(object):
self._token_auth()
def _token_auth(self):
- self.user = CurrentUser(self)
+ if self.api_version == '3':
+ self.user = self._objects.CurrentUser(self)
+ else:
+ self.user = self._objects.CurrentUserManager(self).get()
def version(self):
"""Returns the version and revision of the gitlab server.
@@ -599,3 +606,273 @@ class Gitlab(object):
r = self._raw_put(url, data=data, content_type='application/json')
raise_error_from_response(r, GitlabUpdateError)
return r.json()
+
+ def _build_url(self, path):
+ """Returns the full url from path.
+
+ If path is already a url, return it unchanged. If it's a path, append
+ it to the stored url.
+
+ This is a low-level method, different from _construct_url _build_url
+ have no knowledge of GitlabObject's.
+
+ Returns:
+ str: The full URL
+ """
+ if path.startswith('http://') or path.startswith('https://'):
+ return path
+ else:
+ return '%s%s' % (self._url, path)
+
+ def http_request(self, verb, path, query_data={}, post_data={},
+ streamed=False, **kwargs):
+ """Make an HTTP request to the Gitlab server.
+
+ Args:
+ verb (str): The HTTP method to call ('get', 'post', 'put',
+ 'delete')
+ path (str): Path or full URL to query ('/projects' or
+ 'http://whatever/v4/api/projecs')
+ query_data (dict): Data to send as query parameters
+ post_data (dict): Data to send in the body (will be converted to
+ json)
+ streamed (bool): Whether the data should be streamed
+ **kwargs: Extra data to make the query (e.g. sudo, per_page, page)
+
+ Returns:
+ A requests result object.
+
+ Raises:
+ GitlabHttpError: When the return code is not 2xx
+ """
+
+ def sanitized_url(url):
+ parsed = six.moves.urllib.parse.urlparse(url)
+ new_path = parsed.path.replace('.', '%2E')
+ return parsed._replace(path=new_path).geturl()
+
+ url = self._build_url(path)
+ params = query_data.copy()
+ params.update(kwargs)
+ opts = self._get_session_opts(content_type='application/json')
+ verify = opts.pop('verify')
+ timeout = opts.pop('timeout')
+
+ # Requests assumes that `.` should not be encoded as %2E and will make
+ # changes to urls using this encoding. Using a prepped request we can
+ # get the desired behavior.
+ # The Requests behavior is right but it seems that web servers don't
+ # always agree with this decision (this is the case with a default
+ # gitlab installation)
+ req = requests.Request(verb, url, json=post_data, params=params,
+ **opts)
+ prepped = self.session.prepare_request(req)
+ prepped.url = sanitized_url(prepped.url)
+ result = self.session.send(prepped, stream=streamed, verify=verify,
+ timeout=timeout)
+
+ if 200 <= result.status_code < 300:
+ return result
+
+ if result.status_code == 401:
+ raise GitlabAuthenticationError(response_code=result.status_code,
+ error_message=result.content)
+
+ raise GitlabHttpError(response_code=result.status_code,
+ error_message=result.content)
+
+ def http_get(self, path, query_data={}, streamed=False, **kwargs):
+ """Make a GET request to the Gitlab server.
+
+ Args:
+ path (str): Path or full URL to query ('/projects' or
+ 'http://whatever/v4/api/projecs')
+ query_data (dict): Data to send as query parameters
+ streamed (bool): Whether the data should be streamed
+ **kwargs: Extra data to make the query (e.g. sudo, per_page, page)
+
+ Returns:
+ A requests result object is streamed is True or the content type is
+ not json.
+ The parsed json data otherwise.
+
+ Raises:
+ GitlabHttpError: When the return code is not 2xx
+ GitlabParsingError: If the json data could not be parsed
+ """
+ result = self.http_request('get', path, query_data=query_data,
+ streamed=streamed, **kwargs)
+ if (result.headers['Content-Type'] == 'application/json' and
+ not streamed):
+ try:
+ return result.json()
+ except Exception:
+ raise GitlabParsingError(
+ error_message="Failed to parse the server message")
+ else:
+ return result
+
+ def http_list(self, path, query_data={}, as_list=None, **kwargs):
+ """Make a GET request to the Gitlab server for list-oriented queries.
+
+ Args:
+ path (str): Path or full URL to query ('/projects' or
+ 'http://whatever/v4/api/projecs')
+ query_data (dict): Data to send as query parameters
+ **kwargs: Extra data to make the query (e.g. sudo, per_page, page,
+ all)
+
+ Returns:
+ list: A list of the objects returned by the server. If `as_list` is
+ False and no pagination-related arguments (`page`, `per_page`,
+ `all`) are defined then a GitlabList object (generator) is returned
+ instead. This object will make API calls when needed to fetch the
+ next items from the server.
+
+ Raises:
+ GitlabHttpError: When the return code is not 2xx
+ GitlabParsingError: If the json data could not be parsed
+ """
+
+ # In case we want to change the default behavior at some point
+ as_list = True if as_list is None else as_list
+
+ get_all = kwargs.get('all', False)
+ url = self._build_url(path)
+
+ if get_all is True:
+ return list(GitlabList(self, url, query_data, **kwargs))
+
+ if 'page' in kwargs or 'per_page' in kwargs or as_list is True:
+ # pagination requested, we return a list
+ return list(GitlabList(self, url, query_data, get_next=False,
+ **kwargs))
+
+ # No pagination, generator requested
+ return GitlabList(self, url, query_data, **kwargs)
+
+ def http_post(self, path, query_data={}, post_data={}, **kwargs):
+ """Make a POST request to the Gitlab server.
+
+ Args:
+ path (str): Path or full URL to query ('/projects' or
+ 'http://whatever/v4/api/projecs')
+ query_data (dict): Data to send as query parameters
+ post_data (dict): Data to send in the body (will be converted to
+ json)
+ **kwargs: Extra data to make the query (e.g. sudo, per_page, page)
+
+ Returns:
+ The parsed json returned by the server if json is return, else the
+ raw content
+
+ Raises:
+ GitlabHttpError: When the return code is not 2xx
+ GitlabParsingError: If the json data could not be parsed
+ """
+ result = self.http_request('post', path, query_data=query_data,
+ post_data=post_data, **kwargs)
+ try:
+ if result.headers.get('Content-Type', None) == 'application/json':
+ return result.json()
+ except Exception:
+ raise GitlabParsingError(
+ error_message="Failed to parse the server message")
+ return result
+
+ def http_put(self, path, query_data={}, post_data={}, **kwargs):
+ """Make a PUT request to the Gitlab server.
+
+ Args:
+ path (str): Path or full URL to query ('/projects' or
+ 'http://whatever/v4/api/projecs')
+ query_data (dict): Data to send as query parameters
+ post_data (dict): Data to send in the body (will be converted to
+ json)
+ **kwargs: Extra data to make the query (e.g. sudo, per_page, page)
+
+ Returns:
+ The parsed json returned by the server.
+
+ Raises:
+ GitlabHttpError: When the return code is not 2xx
+ GitlabParsingError: If the json data could not be parsed
+ """
+ result = self.http_request('put', path, query_data=query_data,
+ post_data=post_data, **kwargs)
+ try:
+ return result.json()
+ except Exception:
+ raise GitlabParsingError(
+ error_message="Failed to parse the server message")
+
+ def http_delete(self, path, **kwargs):
+ """Make a PUT request to the Gitlab server.
+
+ Args:
+ path (str): Path or full URL to query ('/projects' or
+ 'http://whatever/v4/api/projecs')
+ **kwargs: Extra data to make the query (e.g. sudo, per_page, page)
+
+ Returns:
+ The requests object.
+
+ Raises:
+ GitlabHttpError: When the return code is not 2xx
+ """
+ return self.http_request('delete', path, **kwargs)
+
+
+class GitlabList(object):
+ """Generator representing a list of remote objects.
+
+ The object handles the links returned by a query to the API, and will call
+ the API again when needed.
+ """
+
+ def __init__(self, gl, url, query_data, get_next=True, **kwargs):
+ self._gl = gl
+ self._query(url, query_data, **kwargs)
+ self._get_next = get_next
+
+ def _query(self, url, query_data={}, **kwargs):
+ result = self._gl.http_request('get', url, query_data=query_data,
+ **kwargs)
+ try:
+ self._next_url = result.links['next']['url']
+ except KeyError:
+ self._next_url = None
+ self._current_page = result.headers.get('X-Page')
+ self._next_page = result.headers.get('X-Next-Page')
+ self._per_page = result.headers.get('X-Per-Page')
+ self._total_pages = result.headers.get('X-Total-Pages')
+ self._total = result.headers.get('X-Total')
+
+ try:
+ self._data = result.json()
+ except Exception:
+ raise GitlabParsingError(
+ error_message="Failed to parse the server message")
+
+ self._current = 0
+
+ def __iter__(self):
+ return self
+
+ def __len__(self):
+ return int(self._total)
+
+ def __next__(self):
+ return self.next()
+
+ def next(self):
+ try:
+ item = self._data[self._current]
+ self._current += 1
+ return item
+ except IndexError:
+ if self._next_url and self._get_next is True:
+ self._query(self._next_url)
+ return self.next()
+
+ raise StopIteration
diff --git a/gitlab/base.py b/gitlab/base.py
index 0d82cf1..df25a36 100644
--- a/gitlab/base.py
+++ b/gitlab/base.py
@@ -531,3 +531,166 @@ class GitlabObject(object):
def __ne__(self, other):
return not self.__eq__(other)
+
+
+class RESTObject(object):
+ """Represents an object built from server data.
+
+ It holds the attributes know from te server, and the updated attributes in
+ another. This allows smart updates, if the object allows it.
+
+ You can redefine ``_id_attr`` in child classes to specify which attribute
+ must be used as uniq ID. ``None`` means that the object can be updated
+ without ID in the url.
+ """
+ _id_attr = 'id'
+
+ def __init__(self, manager, attrs):
+ self.__dict__.update({
+ 'manager': manager,
+ '_attrs': attrs,
+ '_updated_attrs': {},
+ '_module': importlib.import_module(self.__module__)
+ })
+ self.__dict__['_parent_attrs'] = self.manager.parent_attrs
+
+ # TODO(gpocentek): manage the creation of new objects from the received
+ # data (_constructor_types)
+
+ self._create_managers()
+
+ def __getattr__(self, name):
+ try:
+ return self.__dict__['_updated_attrs'][name]
+ except KeyError:
+ try:
+ return self.__dict__['_attrs'][name]
+ except KeyError:
+ try:
+ return self.__dict__['_parent_attrs'][name]
+ except KeyError:
+ raise AttributeError(name)
+
+ def __setattr__(self, name, value):
+ self.__dict__['_updated_attrs'][name] = value
+
+ def __str__(self):
+ data = self._attrs.copy()
+ data.update(self._updated_attrs)
+ return '%s => %s' % (type(self), data)
+
+ def __repr__(self):
+ if self._id_attr:
+ return '<%s %s:%s>' % (self.__class__.__name__,
+ self._id_attr,
+ self.get_id())
+ else:
+ return '<%s>' % self.__class__.__name__
+
+ def _create_managers(self):
+ managers = getattr(self, '_managers', None)
+ if managers is None:
+ return
+
+ for attr, cls_name in self._managers:
+ cls = getattr(self._module, cls_name)
+ manager = cls(self.manager.gitlab, parent=self)
+ self.__dict__[attr] = manager
+
+ def _update_attrs(self, new_attrs):
+ self.__dict__['_updated_attrs'] = {}
+ self.__dict__['_attrs'].update(new_attrs)
+
+ def get_id(self):
+ """Returns the id of the resource."""
+ if self._id_attr is None:
+ return None
+ return getattr(self, self._id_attr)
+
+
+class RESTObjectList(object):
+ """Generator object representing a list of RESTObject's.
+
+ This generator uses the Gitlab pagination system to fetch new data when
+ required.
+
+ Note: you should not instanciate such objects, they are returned by calls
+ to RESTManager.list()
+
+ Args:
+ manager: Manager to attach to the created objects
+ obj_cls: Type of objects to create from the json data
+ _list: A GitlabList object
+ """
+ def __init__(self, manager, obj_cls, _list):
+ """Creates an objects list from a GitlabList.
+
+ You should not create objects of this type, but use managers list()
+ methods instead.
+
+ Args:
+ manager: the RESTManager to attach to the objects
+ obj_cls: the class of the created objects
+ _list: the GitlabList holding the data
+ """
+ self.manager = manager
+ self._obj_cls = obj_cls
+ self._list = _list
+
+ def __iter__(self):
+ return self
+
+ def __len__(self):
+ return len(self._list)
+
+ def __next__(self):
+ return self.next()
+
+ def next(self):
+ data = self._list.next()
+ return self._obj_cls(self.manager, data)
+
+
+class RESTManager(object):
+ """Base class for CRUD operations on objects.
+
+ Derivated class must define ``_path`` and ``_obj_cls``.
+
+ ``_path``: Base URL path on which requests will be sent (e.g. '/projects')
+ ``_obj_cls``: The class of objects that will be created
+ """
+
+ _path = None
+ _obj_cls = None
+
+ def __init__(self, gl, parent=None):
+ """REST manager constructor.
+
+ Args:
+ gl (Gitlab): :class:`~gitlab.Gitlab` connection to use to make
+ requests.
+ parent: REST object to which the manager is attached.
+ """
+ self.gitlab = gl
+ self._parent = parent # for nested managers
+ self._computed_path = self._compute_path()
+
+ @property
+ def parent_attrs(self):
+ return self._parent_attrs
+
+ def _compute_path(self, path=None):
+ self._parent_attrs = {}
+ if path is None:
+ path = self._path
+ if self._parent is None or not hasattr(self, '_from_parent_attrs'):
+ return path
+
+ data = {self_attr: getattr(self._parent, parent_attr)
+ for self_attr, parent_attr in self._from_parent_attrs.items()}
+ self._parent_attrs = data
+ return path % data
+
+ @property
+ def path(self):
+ return self._computed_path
diff --git a/gitlab/cli.py b/gitlab/cli.py
index 8cc89c2..f23fff9 100644
--- a/gitlab/cli.py
+++ b/gitlab/cli.py
@@ -17,447 +17,33 @@
# 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 argparse
-import inspect
-import operator
+import importlib
import re
import sys
-import six
-
-import gitlab
+import gitlab.config
camel_re = re.compile('(.)([A-Z])')
-EXTRA_ACTIONS = {
- gitlab.Group: {'search': {'required': ['query']}},
- gitlab.ProjectBranch: {'protect': {'required': ['id', 'project-id']},
- 'unprotect': {'required': ['id', 'project-id']}},
- gitlab.ProjectBuild: {'cancel': {'required': ['id', 'project-id']},
- 'retry': {'required': ['id', 'project-id']},
- 'artifacts': {'required': ['id', 'project-id']},
- 'trace': {'required': ['id', 'project-id']}},
- gitlab.ProjectCommit: {'diff': {'required': ['id', 'project-id']},
- 'blob': {'required': ['id', 'project-id',
- 'filepath']},
- 'builds': {'required': ['id', 'project-id']},
- 'cherrypick': {'required': ['id', 'project-id',
- 'branch']}},
- gitlab.ProjectIssue: {'subscribe': {'required': ['id', 'project-id']},
- 'unsubscribe': {'required': ['id', 'project-id']},
- 'move': {'required': ['id', 'project-id',
- 'to-project-id']}},
- gitlab.ProjectMergeRequest: {
- 'closes-issues': {'required': ['id', 'project-id']},
- 'cancel': {'required': ['id', 'project-id']},
- 'merge': {'required': ['id', 'project-id'],
- 'optional': ['merge-commit-message',
- 'should-remove-source-branch',
- 'merged-when-build-succeeds']}
- },
- gitlab.ProjectMilestone: {'issues': {'required': ['id', 'project-id']}},
- gitlab.Project: {'search': {'required': ['query']},
- 'owned': {},
- 'all': {'optional': [('all', bool)]},
- 'starred': {},
- 'star': {'required': ['id']},
- 'unstar': {'required': ['id']},
- 'archive': {'required': ['id']},
- 'unarchive': {'required': ['id']},
- 'share': {'required': ['id', 'group-id',
- 'group-access']}},
- gitlab.User: {'block': {'required': ['id']},
- 'unblock': {'required': ['id']},
- 'search': {'required': ['query']},
- 'get-by-username': {'required': ['query']}},
-}
-
-def _die(msg, e=None):
+def die(msg, e=None):
if e:
msg = "%s (%s)" % (msg, e)
sys.stderr.write(msg + "\n")
sys.exit(1)
-def _what_to_cls(what):
+def what_to_cls(what):
return "".join([s.capitalize() for s in what.split("-")])
-def _cls_to_what(cls):
+def cls_to_what(cls):
return camel_re.sub(r'\1-\2', cls.__name__).lower()
-def do_auth(gitlab_id, config_files):
- try:
- gl = gitlab.Gitlab.from_config(gitlab_id, config_files)
- gl.auth()
- return gl
- except Exception as e:
- _die(str(e))
-
-
-class GitlabCLI(object):
- def _get_id(self, cls, args):
- try:
- id = args.pop(cls.idAttr)
- except Exception:
- _die("Missing --%s argument" % cls.idAttr.replace('_', '-'))
-
- return id
-
- def do_create(self, cls, gl, what, args):
- if not cls.canCreate:
- _die("%s objects can't be created" % what)
-
- try:
- o = cls.create(gl, args)
- except Exception as e:
- _die("Impossible to create object", e)
-
- return o
-
- def do_list(self, cls, gl, what, args):
- if not cls.canList:
- _die("%s objects can't be listed" % what)
-
- try:
- l = cls.list(gl, **args)
- except Exception as e:
- _die("Impossible to list objects", e)
-
- return l
-
- def do_get(self, cls, gl, what, args):
- if cls.canGet is False:
- _die("%s objects can't be retrieved" % what)
-
- id = None
- if cls not in [gitlab.CurrentUser] and cls.getRequiresId:
- id = self._get_id(cls, args)
-
- try:
- o = cls.get(gl, id, **args)
- except Exception as e:
- _die("Impossible to get object", e)
-
- return o
-
- def do_delete(self, cls, gl, what, args):
- if not cls.canDelete:
- _die("%s objects can't be deleted" % what)
-
- id = args.pop(cls.idAttr)
- try:
- gl.delete(cls, id, **args)
- except Exception as e:
- _die("Impossible to destroy object", e)
-
- def do_update(self, cls, gl, what, args):
- if not cls.canUpdate:
- _die("%s objects can't be updated" % what)
-
- o = self.do_get(cls, gl, what, args)
- try:
- for k, v in args.items():
- o.__dict__[k] = v
- o.save()
- except Exception as e:
- _die("Impossible to update object", e)
-
- return o
-
- def do_group_search(self, cls, gl, what, args):
- try:
- return gl.groups.search(args['query'])
- except Exception as e:
- _die("Impossible to search projects", e)
-
- def do_project_search(self, cls, gl, what, args):
- try:
- return gl.projects.search(args['query'])
- except Exception as e:
- _die("Impossible to search projects", e)
-
- def do_project_all(self, cls, gl, what, args):
- try:
- return gl.projects.all(all=args.get('all', False))
- except Exception as e:
- _die("Impossible to list all projects", e)
-
- def do_project_starred(self, cls, gl, what, args):
- try:
- return gl.projects.starred()
- except Exception as e:
- _die("Impossible to list starred projects", e)
-
- def do_project_owned(self, cls, gl, what, args):
- try:
- return gl.projects.owned()
- except Exception as e:
- _die("Impossible to list owned projects", e)
-
- def do_project_star(self, cls, gl, what, args):
- try:
- o = self.do_get(cls, gl, what, args)
- o.star()
- except Exception as e:
- _die("Impossible to star project", e)
-
- def do_project_unstar(self, cls, gl, what, args):
- try:
- o = self.do_get(cls, gl, what, args)
- o.unstar()
- except Exception as e:
- _die("Impossible to unstar project", e)
-
- def do_project_archive(self, cls, gl, what, args):
- try:
- o = self.do_get(cls, gl, what, args)
- o.archive_()
- except Exception as e:
- _die("Impossible to archive project", e)
-
- def do_project_unarchive(self, cls, gl, what, args):
- try:
- o = self.do_get(cls, gl, what, args)
- o.unarchive_()
- except Exception as e:
- _die("Impossible to unarchive project", e)
-
- def do_project_share(self, cls, gl, what, args):
- try:
- o = self.do_get(cls, gl, what, args)
- o.share(args['group_id'], args['group_access'])
- except Exception as e:
- _die("Impossible to share project", e)
-
- def do_user_block(self, cls, gl, what, args):
- try:
- o = self.do_get(cls, gl, what, args)
- o.block()
- except Exception as e:
- _die("Impossible to block user", e)
-
- def do_user_unblock(self, cls, gl, what, args):
- try:
- o = self.do_get(cls, gl, what, args)
- o.unblock()
- except Exception as e:
- _die("Impossible to block user", e)
-
- def do_project_commit_diff(self, cls, gl, what, args):
- try:
- o = self.do_get(cls, gl, what, args)
- return [x['diff'] for x in o.diff()]
- except Exception as e:
- _die("Impossible to get commit diff", e)
-
- def do_project_commit_blob(self, cls, gl, what, args):
- try:
- o = self.do_get(cls, gl, what, args)
- return o.blob(args['filepath'])
- except Exception as e:
- _die("Impossible to get commit blob", e)
-
- def do_project_commit_builds(self, cls, gl, what, args):
- try:
- o = self.do_get(cls, gl, what, args)
- return o.builds()
- except Exception as e:
- _die("Impossible to get commit builds", e)
-
- def do_project_commit_cherrypick(self, cls, gl, what, args):
- try:
- o = self.do_get(cls, gl, what, args)
- o.cherry_pick(branch=args['branch'])
- except Exception as e:
- _die("Impossible to cherry-pick commit", e)
-
- def do_project_build_cancel(self, cls, gl, what, args):
- try:
- o = self.do_get(cls, gl, what, args)
- return o.cancel()
- except Exception as e:
- _die("Impossible to cancel project build", e)
-
- def do_project_build_retry(self, cls, gl, what, args):
- try:
- o = self.do_get(cls, gl, what, args)
- return o.retry()
- except Exception as e:
- _die("Impossible to retry project build", e)
-
- def do_project_build_artifacts(self, cls, gl, what, args):
- try:
- o = self.do_get(cls, gl, what, args)
- return o.artifacts()
- except Exception as e:
- _die("Impossible to get project build artifacts", e)
-
- def do_project_build_trace(self, cls, gl, what, args):
- try:
- o = self.do_get(cls, gl, what, args)
- return o.trace()
- except Exception as e:
- _die("Impossible to get project build trace", e)
-
- def do_project_issue_subscribe(self, cls, gl, what, args):
- try:
- o = self.do_get(cls, gl, what, args)
- o.subscribe()
- except Exception as e:
- _die("Impossible to subscribe to issue", e)
-
- def do_project_issue_unsubscribe(self, cls, gl, what, args):
- try:
- o = self.do_get(cls, gl, what, args)
- o.unsubscribe()
- except Exception as e:
- _die("Impossible to subscribe to issue", e)
-
- def do_project_issue_move(self, cls, gl, what, args):
- try:
- o = self.do_get(cls, gl, what, args)
- o.move(args['to_project_id'])
- except Exception as e:
- _die("Impossible to move issue", e)
-
- def do_project_merge_request_closesissues(self, cls, gl, what, args):
- try:
- o = self.do_get(cls, gl, what, args)
- return o.closes_issues()
- except Exception as e:
- _die("Impossible to list issues closed by merge request", e)
-
- def do_project_merge_request_cancel(self, cls, gl, what, args):
- try:
- o = self.do_get(cls, gl, what, args)
- return o.cancel_merge_when_build_succeeds()
- except Exception as e:
- _die("Impossible to cancel merge request", e)
-
- def do_project_merge_request_merge(self, cls, gl, what, args):
- try:
- o = self.do_get(cls, gl, what, args)
- should_remove = args.get('should_remove_source_branch', False)
- build_succeeds = args.get('merged_when_build_succeeds', False)
- return o.merge(
- merge_commit_message=args.get('merge_commit_message', ''),
- should_remove_source_branch=should_remove,
- merged_when_build_succeeds=build_succeeds)
- except Exception as e:
- _die("Impossible to validate merge request", e)
-
- def do_project_milestone_issues(self, cls, gl, what, args):
- try:
- o = self.do_get(cls, gl, what, args)
- return o.issues()
- except Exception as e:
- _die("Impossible to get milestone issues", e)
-
- def do_user_search(self, cls, gl, what, args):
- try:
- return gl.users.search(args['query'])
- except Exception as e:
- _die("Impossible to search users", e)
-
- def do_user_getbyusername(self, cls, gl, what, args):
- try:
- return gl.users.search(args['query'])
- except Exception as e:
- _die("Impossible to get user %s" % args['query'], e)
-
-
-def _populate_sub_parser_by_class(cls, sub_parser):
- for action_name in ['list', 'get', 'create', 'update', 'delete']:
- attr = 'can' + action_name.capitalize()
- if not getattr(cls, attr):
- continue
- sub_parser_action = sub_parser.add_parser(action_name)
- [sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
- required=True)
- for x in cls.requiredUrlAttrs]
- sub_parser_action.add_argument("--sudo", required=False)
-
- if action_name == "list":
- [sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
- required=True)
- for x in cls.requiredListAttrs]
- sub_parser_action.add_argument("--page", required=False)
- sub_parser_action.add_argument("--per-page", required=False)
- sub_parser_action.add_argument("--all", required=False,
- action='store_true')
-
- if action_name in ["get", "delete"]:
- if cls not in [gitlab.CurrentUser]:
- if cls.getRequiresId:
- id_attr = cls.idAttr.replace('_', '-')
- sub_parser_action.add_argument("--%s" % id_attr,
- required=True)
- [sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
- required=True)
- for x in cls.requiredGetAttrs if x != cls.idAttr]
-
- if action_name == "get":
- [sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
- required=False)
- for x in cls.optionalGetAttrs]
-
- if action_name == "list":
- [sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
- required=False)
- for x in cls.optionalListAttrs]
-
- if action_name == "create":
- [sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
- required=True)
- for x in cls.requiredCreateAttrs]
- [sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
- required=False)
- for x in cls.optionalCreateAttrs]
-
- if action_name == "update":
- id_attr = cls.idAttr.replace('_', '-')
- sub_parser_action.add_argument("--%s" % id_attr,
- required=True)
-
- attrs = (cls.requiredUpdateAttrs
- if (cls.requiredUpdateAttrs or cls.optionalUpdateAttrs)
- else cls.requiredCreateAttrs)
- [sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
- required=True)
- for x in attrs if x != cls.idAttr]
-
- attrs = (cls.optionalUpdateAttrs
- if (cls.requiredUpdateAttrs or cls.optionalUpdateAttrs)
- else cls.optionalCreateAttrs)
- [sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
- required=False)
- for x in attrs]
-
- if cls in EXTRA_ACTIONS:
- def _add_arg(parser, required, data):
- extra_args = {}
- if isinstance(data, tuple):
- if data[1] is bool:
- extra_args = {'action': 'store_true'}
- data = data[0]
-
- parser.add_argument("--%s" % data, required=required, **extra_args)
-
- for action_name in sorted(EXTRA_ACTIONS[cls]):
- sub_parser_action = sub_parser.add_parser(action_name)
- d = EXTRA_ACTIONS[cls][action_name]
- [_add_arg(sub_parser_action, True, arg)
- for arg in d.get('required', [])]
- [_add_arg(sub_parser_action, False, arg)
- for arg in d.get('optional', [])]
-
-
-def _build_parser(args=sys.argv[1:]):
+def _get_base_parser():
parser = argparse.ArgumentParser(
description="GitLab API Command Line Interface")
parser.add_argument("--version", help="Display the version.",
@@ -474,35 +60,12 @@ def _build_parser(args=sys.argv[1:]):
"will be used."),
required=False)
- subparsers = parser.add_subparsers(title='object', dest='what',
- help="Object to manipulate.")
- subparsers.required = True
-
- # populate argparse for all Gitlab Object
- classes = []
- for cls in gitlab.__dict__.values():
- try:
- if gitlab.GitlabObject in inspect.getmro(cls):
- classes.append(cls)
- except AttributeError:
- pass
- classes.sort(key=operator.attrgetter("__name__"))
-
- for cls in classes:
- arg_name = _cls_to_what(cls)
- object_group = subparsers.add_parser(arg_name)
-
- object_subparsers = object_group.add_subparsers(
- dest='action', help="Action to execute.")
- _populate_sub_parser_by_class(cls, object_subparsers)
- object_subparsers.required = True
-
return parser
-def _parse_args(args=sys.argv[1:]):
- parser = _build_parser()
- return parser.parse_args(args)
+def _get_parser(cli_module):
+ parser = _get_base_parser()
+ return cli_module.extend_parser(parser)
def main():
@@ -510,56 +73,33 @@ def main():
print(gitlab.__version__)
exit(0)
- arg = _parse_args()
- args = arg.__dict__
-
- config_files = arg.config_file
- gitlab_id = arg.gitlab
- verbose = arg.verbose
- action = arg.action
- what = arg.what
-
+ parser = _get_base_parser()
+ (options, args) = parser.parse_known_args(sys.argv)
+
+ config = gitlab.config.GitlabConfigParser(options.gitlab,
+ options.config_file)
+ cli_module = importlib.import_module('gitlab.v%s.cli' % config.api_version)
+ parser = _get_parser(cli_module)
+ args = parser.parse_args(sys.argv[1:])
+ config_files = args.config_file
+ gitlab_id = args.gitlab
+ verbose = args.verbose
+ action = args.action
+ what = args.what
+
+ args = args.__dict__
# Remove CLI behavior-related args
- for item in ("gitlab", "config_file", "verbose", "what", "action",
- "version"):
+ for item in ('gitlab', 'config_file', 'verbose', 'what', 'action',
+ 'version'):
args.pop(item)
-
args = {k: v for k, v in args.items() if v is not None}
- cls = None
try:
- cls = gitlab.__dict__[_what_to_cls(what)]
- except Exception:
- _die("Unknown object: %s" % what)
-
- gl = do_auth(gitlab_id, config_files)
-
- cli = GitlabCLI()
- method = None
- what = what.replace('-', '_')
- action = action.lower().replace('-', '')
- for test in ["do_%s_%s" % (what, action),
- "do_%s" % action]:
- if hasattr(cli, test):
- method = test
- break
-
- if method is None:
- sys.stderr.write("Don't know how to deal with this!\n")
- sys.exit(1)
-
- ret_val = getattr(cli, method)(cls, gl, what, args)
+ gl = gitlab.Gitlab.from_config(gitlab_id, config_files)
+ gl.auth()
+ except Exception as e:
+ die(str(e))
- if isinstance(ret_val, list):
- for o in ret_val:
- if isinstance(o, gitlab.GitlabObject):
- o.display(verbose)
- print("")
- else:
- print(o)
- elif isinstance(ret_val, gitlab.GitlabObject):
- ret_val.display(verbose)
- elif isinstance(ret_val, six.string_types):
- print(ret_val)
+ cli_module.run(gl, what, action, args, verbose)
sys.exit(0)
diff --git a/gitlab/exceptions.py b/gitlab/exceptions.py
index c7d1da6..6c00129 100644
--- a/gitlab/exceptions.py
+++ b/gitlab/exceptions.py
@@ -39,6 +39,10 @@ class GitlabAuthenticationError(GitlabError):
pass
+class GitlabParsingError(GitlabError):
+ pass
+
+
class GitlabConnectionError(GitlabError):
pass
@@ -47,6 +51,10 @@ class GitlabOperationError(GitlabError):
pass
+class GitlabHttpError(GitlabError):
+ pass
+
+
class GitlabListError(GitlabOperationError):
pass
@@ -202,3 +210,23 @@ def raise_error_from_response(response, error, expected_code=200):
raise error(error_message=message,
response_code=response.status_code,
response_body=response.content)
+
+
+def on_http_error(error):
+ """Manage GitlabHttpError exceptions.
+
+ This decorator function can be used to catch GitlabHttpError exceptions
+ raise specialized exceptions instead.
+
+ Args:
+ error(Exception): The exception type to raise -- must inherit from
+ GitlabError
+ """
+ def wrap(f):
+ def wrapped_f(*args, **kwargs):
+ try:
+ return f(*args, **kwargs)
+ except GitlabHttpError as e:
+ raise error(e.response_code, e.error_message)
+ return wrapped_f
+ return wrap
diff --git a/gitlab/mixins.py b/gitlab/mixins.py
new file mode 100644
index 0000000..9dd05af
--- /dev/null
+++ b/gitlab/mixins.py
@@ -0,0 +1,438 @@
+# -*- 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 gitlab
+from gitlab import base
+from gitlab import exceptions as exc
+
+
+class GetMixin(object):
+ @exc.on_http_error(exc.GitlabGetError)
+ def get(self, id, lazy=False, **kwargs):
+ """Retrieve a single object.
+
+ Args:
+ id (int or str): ID of the object to retrieve
+ lazy (bool): If True, don't request the server, but create a
+ shallow object giving access to the managers. This is
+ useful if you want to avoid useless calls to the API.
+ **kwargs: Extra options to send to the Gitlab server (e.g. sudo)
+
+ Returns:
+ object: The generated RESTObject.
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabGetError: If the server cannot perform the request
+ """
+ path = '%s/%s' % (self.path, id)
+ if lazy is True:
+ return self._obj_cls(self, {self._obj_cls._id_attr: id})
+ server_data = self.gitlab.http_get(path, **kwargs)
+ return self._obj_cls(self, server_data)
+
+
+class GetWithoutIdMixin(object):
+ @exc.on_http_error(exc.GitlabGetError)
+ def get(self, **kwargs):
+ """Retrieve a single object.
+
+ Args:
+ **kwargs: Extra options to send to the Gitlab server (e.g. sudo)
+
+ Returns:
+ object: The generated RESTObject
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabGetError: If the server cannot perform the request
+ """
+ server_data = self.gitlab.http_get(self.path, **kwargs)
+ return self._obj_cls(self, server_data)
+
+
+class ListMixin(object):
+ @exc.on_http_error(exc.GitlabListError)
+ def list(self, **kwargs):
+ """Retrieve a list of objects.
+
+ Args:
+ all (bool): If True, return all the items, without pagination
+ per_page (int): Number of items to retrieve per request
+ page (int): ID of the page to return (starts with page 1)
+ as_list (bool): If set to False and no pagination option is
+ defined, return a generator instead of a list
+ **kwargs: Extra options to send to the Gitlab server (e.g. sudo)
+
+ Returns:
+ list: The list of objects, or a generator if `as_list` is False
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabListError: If the server cannot perform the request
+ """
+
+ # Allow to overwrite the path, handy for custom listings
+ path = kwargs.pop('path', self.path)
+ obj = self.gitlab.http_list(path, **kwargs)
+ if isinstance(obj, list):
+ return [self._obj_cls(self, item) for item in obj]
+ else:
+ return base.RESTObjectList(self, self._obj_cls, obj)
+
+
+class GetFromListMixin(ListMixin):
+ def get(self, id, **kwargs):
+ """Retrieve a single object.
+
+ Args:
+ id (int or str): ID of the object to retrieve
+ **kwargs: Extra options to send to the Gitlab server (e.g. sudo)
+
+ Returns:
+ object: The generated RESTObject
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabGetError: If the server cannot perform the request
+ """
+ gen = self.list()
+ for obj in gen:
+ if str(obj.get_id()) == str(id):
+ return obj
+
+ raise exc.GitlabGetError(response_code=404, error_message="Not found")
+
+
+class RetrieveMixin(ListMixin, GetMixin):
+ pass
+
+
+class CreateMixin(object):
+ def _check_missing_create_attrs(self, data):
+ required, optional = self.get_create_attrs()
+ missing = []
+ for attr in required:
+ if attr not in data:
+ missing.append(attr)
+ continue
+ if missing:
+ raise AttributeError("Missing attributes: %s" % ", ".join(missing))
+
+ def get_create_attrs(self):
+ """Return the required and optional arguments.
+
+ Returns:
+ tuple: 2 items: list of required arguments and list of optional
+ arguments for creation (in that order)
+ """
+ return getattr(self, '_create_attrs', (tuple(), tuple()))
+
+ @exc.on_http_error(exc.GitlabCreateError)
+ def create(self, data, **kwargs):
+ """Create a new object.
+
+ Args:
+ data (dict): parameters to send to the server to create the
+ resource
+ **kwargs: Extra options to send to the Gitlab server (e.g. sudo)
+
+ Returns:
+ RESTObject: a new instance of the managed object class built with
+ the data sent by the server
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabCreateError: If the server cannot perform the request
+ """
+ self._check_missing_create_attrs(data)
+ if hasattr(self, '_sanitize_data'):
+ data = self._sanitize_data(data, 'create')
+ # Handle specific URL for creation
+ path = kwargs.pop('path', self.path)
+ server_data = self.gitlab.http_post(path, post_data=data, **kwargs)
+ return self._obj_cls(self, server_data)
+
+
+class UpdateMixin(object):
+ def _check_missing_update_attrs(self, data):
+ required, optional = self.get_update_attrs()
+ missing = []
+ for attr in required:
+ if attr not in data:
+ missing.append(attr)
+ continue
+ if missing:
+ raise AttributeError("Missing attributes: %s" % ", ".join(missing))
+
+ def get_update_attrs(self):
+ """Return the required and optional arguments.
+
+ Returns:
+ tuple: 2 items: list of required arguments and list of optional
+ arguments for update (in that order)
+ """
+ return getattr(self, '_update_attrs', (tuple(), tuple()))
+
+ @exc.on_http_error(exc.GitlabUpdateError)
+ def update(self, id=None, new_data={}, **kwargs):
+ """Update an object on the server.
+
+ Args:
+ id: ID of the object to update (can be None if not required)
+ new_data: the update data for the object
+ **kwargs: Extra options to send to the Gitlab server (e.g. sudo)
+
+ Returns:
+ dict: The new object data (*not* a RESTObject)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabUpdateError: If the server cannot perform the request
+ """
+
+ if id is None:
+ path = self.path
+ else:
+ path = '%s/%s' % (self.path, id)
+
+ self._check_missing_update_attrs(new_data)
+ if hasattr(self, '_sanitize_data'):
+ data = self._sanitize_data(new_data, 'update')
+ else:
+ data = new_data
+
+ return self.gitlab.http_put(path, post_data=data, **kwargs)
+
+
+class DeleteMixin(object):
+ @exc.on_http_error(exc.GitlabDeleteError)
+ def delete(self, id, **kwargs):
+ """Delete an object on the server.
+
+ Args:
+ id: ID of the object to delete
+ **kwargs: Extra options to send to the Gitlab server (e.g. sudo)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabDeleteError: If the server cannot perform the request
+ """
+ path = '%s/%s' % (self.path, id)
+ self.gitlab.http_delete(path, **kwargs)
+
+
+class CRUDMixin(GetMixin, ListMixin, CreateMixin, UpdateMixin, DeleteMixin):
+ pass
+
+
+class NoUpdateMixin(GetMixin, ListMixin, CreateMixin, DeleteMixin):
+ pass
+
+
+class SaveMixin(object):
+ """Mixin for RESTObject's that can be updated."""
+ def _get_updated_data(self):
+ updated_data = {}
+ required, optional = self.manager.get_update_attrs()
+ for attr in required:
+ # Get everything required, no matter if it's been updated
+ updated_data[attr] = getattr(self, attr)
+ # Add the updated attributes
+ updated_data.update(self._updated_attrs)
+
+ return updated_data
+
+ def save(self, **kwargs):
+ """Save the changes made to the object to the server.
+
+ The object is updated to match what the server returns.
+
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ Raise:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabUpdateError: If the server cannot perform the request
+ """
+ updated_data = self._get_updated_data()
+
+ # call the manager
+ obj_id = self.get_id()
+ server_data = self.manager.update(obj_id, updated_data, **kwargs)
+ if server_data is not None:
+ self._update_attrs(server_data)
+
+
+class ObjectDeleteMixin(object):
+ """Mixin for RESTObject's that can be deleted."""
+ def delete(self, **kwargs):
+ """Delete the object from the server.
+
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabDeleteError: If the server cannot perform the request
+ """
+ self.manager.delete(self.get_id())
+
+
+class AccessRequestMixin(object):
+ @exc.on_http_error(exc.GitlabUpdateError)
+ def approve(self, access_level=gitlab.DEVELOPER_ACCESS, **kwargs):
+ """Approve an access request.
+
+ Attrs:
+ access_level (int): The access level for the user
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabUpdateError: If the server fails to perform the request
+ """
+
+ path = '%s/%s/approve' % (self.manager.path, self.id)
+ data = {'access_level': access_level}
+ server_data = self.manager.gitlab.http_put(path, post_data=data,
+ **kwargs)
+ self._update_attrs(server_data)
+
+
+class SubscribableMixin(object):
+ @exc.on_http_error(exc.GitlabSubscribeError)
+ def subscribe(self, **kwargs):
+ """Subscribe to the object notifications.
+
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabSubscribeError: If the subscription cannot be done
+ """
+ path = '%s/%s/subscribe' % (self.manager.path, self.get_id())
+ server_data = self.manager.gitlab.http_post(path, **kwargs)
+ self._update_attrs(server_data)
+
+ @exc.on_http_error(exc.GitlabUnsubscribeError)
+ def unsubscribe(self, **kwargs):
+ """Unsubscribe from the object notifications.
+
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabUnsubscribeError: If the unsubscription cannot be done
+ """
+ path = '%s/%s/unsubscribe' % (self.manager.path, self.get_id())
+ server_data = self.manager.gitlab.http_post(path, **kwargs)
+ self._update_attrs(server_data)
+
+
+class TodoMixin(object):
+ @exc.on_http_error(exc.GitlabHttpError)
+ def todo(self, **kwargs):
+ """Create a todo associated to the object.
+
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabTodoError: If the todo cannot be set
+ """
+ path = '%s/%s/todo' % (self.manager.path, self.get_id())
+ self.manager.gitlab.http_post(path, **kwargs)
+
+
+class TimeTrackingMixin(object):
+ @exc.on_http_error(exc.GitlabTimeTrackingError)
+ def time_stats(self, **kwargs):
+ """Get time stats for the object.
+
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabTimeTrackingError: If the time tracking update cannot be done
+ """
+ path = '%s/%s/time_stats' % (self.manager.path, self.get_id())
+ return self.manager.gitlab.http_get(path, **kwargs)
+
+ @exc.on_http_error(exc.GitlabTimeTrackingError)
+ def time_estimate(self, duration, **kwargs):
+ """Set an estimated time of work for the object.
+
+ Args:
+ duration (str): Duration in human format (e.g. 3h30)
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabTimeTrackingError: If the time tracking update cannot be done
+ """
+ path = '%s/%s/time_estimate' % (self.manager.path, self.get_id())
+ data = {'duration': duration}
+ return self.manager.gitlab.http_post(path, post_data=data, **kwargs)
+
+ @exc.on_http_error(exc.GitlabTimeTrackingError)
+ def reset_time_estimate(self, **kwargs):
+ """Resets estimated time for the object to 0 seconds.
+
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabTimeTrackingError: If the time tracking update cannot be done
+ """
+ path = '%s/%s/rest_time_estimate' % (self.manager.path, self.get_id())
+ return self.manager.gitlab.http_post(path, **kwargs)
+
+ @exc.on_http_error(exc.GitlabTimeTrackingError)
+ def add_spent_time(self, duration, **kwargs):
+ """Add time spent working on the object.
+
+ Args:
+ duration (str): Duration in human format (e.g. 3h30)
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabTimeTrackingError: If the time tracking update cannot be done
+ """
+ path = '%s/%s/add_spent_time' % (self.manager.path, self.get_id())
+ data = {'duration': duration}
+ return self.manager.gitlab.http_post(path, post_data=data, **kwargs)
+
+ @exc.on_http_error(exc.GitlabTimeTrackingError)
+ def reset_spent_time(self, **kwargs):
+ """Resets the time spent working on the object.
+
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabTimeTrackingError: If the time tracking update cannot be done
+ """
+ path = '%s/%s/reset_spent_time' % (self.manager.path, self.get_id())
+ return self.manager.gitlab.http_post(path, **kwargs)
diff --git a/gitlab/tests/test_base.py b/gitlab/tests/test_base.py
new file mode 100644
index 0000000..c55f000
--- /dev/null
+++ b/gitlab/tests/test_base.py
@@ -0,0 +1,129 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 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/>.
+
+try:
+ import unittest
+except ImportError:
+ import unittest2 as unittest
+
+from gitlab import base
+
+
+class FakeGitlab(object):
+ pass
+
+
+class FakeObject(base.RESTObject):
+ pass
+
+
+class FakeManager(base.RESTManager):
+ _obj_cls = FakeObject
+ _path = '/tests'
+
+
+class TestRESTManager(unittest.TestCase):
+ def test_computed_path_simple(self):
+ class MGR(base.RESTManager):
+ _path = '/tests'
+ _obj_cls = object
+
+ mgr = MGR(FakeGitlab())
+ self.assertEqual(mgr._computed_path, '/tests')
+
+ def test_computed_path_with_parent(self):
+ class MGR(base.RESTManager):
+ _path = '/tests/%(test_id)s/cases'
+ _obj_cls = object
+ _from_parent_attrs = {'test_id': 'id'}
+
+ class Parent(object):
+ id = 42
+
+ class BrokenParent(object):
+ no_id = 0
+
+ mgr = MGR(FakeGitlab(), parent=Parent())
+ self.assertEqual(mgr._computed_path, '/tests/42/cases')
+
+ self.assertRaises(AttributeError, MGR, FakeGitlab(),
+ parent=BrokenParent())
+
+ def test_path_property(self):
+ class MGR(base.RESTManager):
+ _path = '/tests'
+ _obj_cls = object
+
+ mgr = MGR(FakeGitlab())
+ self.assertEqual(mgr.path, '/tests')
+
+
+class TestRESTObject(unittest.TestCase):
+ def setUp(self):
+ self.gitlab = FakeGitlab()
+ self.manager = FakeManager(self.gitlab)
+
+ def test_instanciate(self):
+ obj = FakeObject(self.manager, {'foo': 'bar'})
+
+ self.assertDictEqual({'foo': 'bar'}, obj._attrs)
+ self.assertDictEqual({}, obj._updated_attrs)
+ self.assertEqual(None, obj._create_managers())
+ self.assertEqual(self.manager, obj.manager)
+ self.assertEqual(self.gitlab, obj.manager.gitlab)
+
+ def test_attrs(self):
+ obj = FakeObject(self.manager, {'foo': 'bar'})
+
+ self.assertEqual('bar', obj.foo)
+ self.assertRaises(AttributeError, getattr, obj, 'bar')
+
+ obj.bar = 'baz'
+ self.assertEqual('baz', obj.bar)
+ self.assertDictEqual({'foo': 'bar'}, obj._attrs)
+ self.assertDictEqual({'bar': 'baz'}, obj._updated_attrs)
+
+ def test_get_id(self):
+ obj = FakeObject(self.manager, {'foo': 'bar'})
+ obj.id = 42
+ self.assertEqual(42, obj.get_id())
+
+ obj.id = None
+ self.assertEqual(None, obj.get_id())
+
+ def test_custom_id_attr(self):
+ class OtherFakeObject(FakeObject):
+ _id_attr = 'foo'
+
+ obj = OtherFakeObject(self.manager, {'foo': 'bar'})
+ self.assertEqual('bar', obj.get_id())
+
+ def test_update_attrs(self):
+ obj = FakeObject(self.manager, {'foo': 'bar'})
+ obj.bar = 'baz'
+ obj._update_attrs({'foo': 'foo', 'bar': 'bar'})
+ self.assertDictEqual({'foo': 'foo', 'bar': 'bar'}, obj._attrs)
+ self.assertDictEqual({}, obj._updated_attrs)
+
+ def test_create_managers(self):
+ class ObjectWithManager(FakeObject):
+ _managers = (('fakes', 'FakeManager'), )
+
+ obj = ObjectWithManager(self.manager, {'foo': 'bar'})
+ self.assertIsInstance(obj.fakes, FakeManager)
+ self.assertEqual(obj.fakes.gitlab, self.gitlab)
+ self.assertEqual(obj.fakes._parent, obj)
diff --git a/gitlab/tests/test_cli.py b/gitlab/tests/test_cli.py
index 701655d..e6e290a 100644
--- a/gitlab/tests/test_cli.py
+++ b/gitlab/tests/test_cli.py
@@ -28,12 +28,13 @@ except ImportError:
import unittest2 as unittest
from gitlab import cli
+import gitlab.v3.cli
class TestCLI(unittest.TestCase):
def test_what_to_cls(self):
- self.assertEqual("Foo", cli._what_to_cls("foo"))
- self.assertEqual("FooBar", cli._what_to_cls("foo-bar"))
+ self.assertEqual("Foo", cli.what_to_cls("foo"))
+ self.assertEqual("FooBar", cli.what_to_cls("foo-bar"))
def test_cls_to_what(self):
class Class(object):
@@ -42,32 +43,33 @@ class TestCLI(unittest.TestCase):
class TestClass(object):
pass
- self.assertEqual("test-class", cli._cls_to_what(TestClass))
- self.assertEqual("class", cli._cls_to_what(Class))
+ self.assertEqual("test-class", cli.cls_to_what(TestClass))
+ self.assertEqual("class", cli.cls_to_what(Class))
def test_die(self):
with self.assertRaises(SystemExit) as test:
- cli._die("foobar")
+ cli.die("foobar")
self.assertEqual(test.exception.code, 1)
- def test_extra_actions(self):
- for cls, data in six.iteritems(cli.EXTRA_ACTIONS):
- for key in data:
- self.assertIsInstance(data[key], dict)
-
- def test_parsing(self):
- args = cli._parse_args(['-v', '-g', 'gl_id',
- '-c', 'foo.cfg', '-c', 'bar.cfg',
- 'project', 'list'])
+ def test_base_parser(self):
+ parser = cli._get_base_parser()
+ args = parser.parse_args(['-v', '-g', 'gl_id',
+ '-c', 'foo.cfg', '-c', 'bar.cfg'])
self.assertTrue(args.verbose)
self.assertEqual(args.gitlab, 'gl_id')
self.assertEqual(args.config_file, ['foo.cfg', 'bar.cfg'])
+
+
+class TestV3CLI(unittest.TestCase):
+ def test_parse_args(self):
+ parser = cli._get_parser(gitlab.v3.cli)
+ args = parser.parse_args(['project', 'list'])
self.assertEqual(args.what, 'project')
self.assertEqual(args.action, 'list')
def test_parser(self):
- parser = cli._build_parser()
+ parser = cli._get_parser(gitlab.v3.cli)
subparsers = None
for action in parser._actions:
if type(action) == argparse._SubParsersAction:
@@ -93,3 +95,8 @@ class TestCLI(unittest.TestCase):
actions = user_subparsers.choices['create']._option_string_actions
self.assertFalse(actions['--twitter'].required)
self.assertTrue(actions['--username'].required)
+
+ def test_extra_actions(self):
+ for cls, data in six.iteritems(gitlab.v3.cli.EXTRA_ACTIONS):
+ for key in data:
+ self.assertIsInstance(data[key], dict)
diff --git a/gitlab/tests/test_gitlab.py b/gitlab/tests/test_gitlab.py
index c2cd19b..6bc427d 100644
--- a/gitlab/tests/test_gitlab.py
+++ b/gitlab/tests/test_gitlab.py
@@ -171,6 +171,277 @@ class TestGitlabRawMethods(unittest.TestCase):
self.assertEqual(resp.status_code, 404)
+class TestGitlabList(unittest.TestCase):
+ def setUp(self):
+ self.gl = Gitlab("http://localhost", private_token="private_token",
+ api_version=4)
+
+ def test_build_list(self):
+ @urlmatch(scheme='http', netloc="localhost", path="/api/v4/tests",
+ method="get")
+ def resp_1(url, request):
+ headers = {'content-type': 'application/json',
+ 'X-Page': 1,
+ 'X-Next-Page': 2,
+ 'X-Per-Page': 1,
+ 'X-Total-Pages': 2,
+ 'X-Total': 2,
+ 'Link': (
+ '<http://localhost/api/v4/tests?per_page=1&page=2>;'
+ ' rel="next"')}
+ content = '[{"a": "b"}]'
+ return response(200, content, headers, None, 5, request)
+
+ @urlmatch(scheme='http', netloc="localhost", path="/api/v4/tests",
+ method='get', query=r'.*page=2')
+ def resp_2(url, request):
+ headers = {'content-type': 'application/json',
+ 'X-Page': 2,
+ 'X-Next-Page': 2,
+ 'X-Per-Page': 1,
+ 'X-Total-Pages': 2,
+ 'X-Total': 2}
+ content = '[{"c": "d"}]'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_1):
+ obj = self.gl.http_list('/tests', as_list=False)
+ self.assertEqual(len(obj), 2)
+ self.assertEqual(obj._next_url,
+ 'http://localhost/api/v4/tests?per_page=1&page=2')
+
+ with HTTMock(resp_2):
+ l = list(obj)
+ self.assertEqual(len(l), 2)
+ self.assertEqual(l[0]['a'], 'b')
+ self.assertEqual(l[1]['c'], 'd')
+
+
+class TestGitlabHttpMethods(unittest.TestCase):
+ def setUp(self):
+ self.gl = Gitlab("http://localhost", private_token="private_token",
+ api_version=4)
+
+ def test_build_url(self):
+ r = self.gl._build_url('http://localhost/api/v4')
+ self.assertEqual(r, 'http://localhost/api/v4')
+ r = self.gl._build_url('https://localhost/api/v4')
+ self.assertEqual(r, 'https://localhost/api/v4')
+ r = self.gl._build_url('/projects')
+ self.assertEqual(r, 'http://localhost/api/v4/projects')
+
+ def test_http_request(self):
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects",
+ method="get")
+ def resp_cont(url, request):
+ headers = {'content-type': 'application/json'}
+ content = '[{"name": "project1"}]'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ http_r = self.gl.http_request('get', '/projects')
+ http_r.json()
+ self.assertEqual(http_r.status_code, 200)
+
+ def test_http_request_404(self):
+ @urlmatch(scheme="http", netloc="localhost",
+ path="/api/v4/not_there", method="get")
+ def resp_cont(url, request):
+ content = {'Here is wh it failed'}
+ return response(404, content, {}, None, 5, request)
+
+ with HTTMock(resp_cont):
+ self.assertRaises(GitlabHttpError,
+ self.gl.http_request,
+ 'get', '/not_there')
+
+ def test_get_request(self):
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects",
+ method="get")
+ def resp_cont(url, request):
+ headers = {'content-type': 'application/json'}
+ content = '{"name": "project1"}'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ result = self.gl.http_get('/projects')
+ self.assertIsInstance(result, dict)
+ self.assertEqual(result['name'], 'project1')
+
+ def test_get_request_raw(self):
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects",
+ method="get")
+ def resp_cont(url, request):
+ headers = {'content-type': 'application/octet-stream'}
+ content = 'content'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ result = self.gl.http_get('/projects')
+ self.assertEqual(result.content.decode('utf-8'), 'content')
+
+ def test_get_request_404(self):
+ @urlmatch(scheme="http", netloc="localhost",
+ path="/api/v4/not_there", method="get")
+ def resp_cont(url, request):
+ content = {'Here is wh it failed'}
+ return response(404, content, {}, None, 5, request)
+
+ with HTTMock(resp_cont):
+ self.assertRaises(GitlabHttpError, self.gl.http_get, '/not_there')
+
+ def test_get_request_invalid_data(self):
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects",
+ method="get")
+ def resp_cont(url, request):
+ headers = {'content-type': 'application/json'}
+ content = '["name": "project1"]'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ self.assertRaises(GitlabParsingError, self.gl.http_get,
+ '/projects')
+
+ def test_list_request(self):
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects",
+ method="get")
+ def resp_cont(url, request):
+ headers = {'content-type': 'application/json', 'X-Total': 1}
+ content = '[{"name": "project1"}]'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ result = self.gl.http_list('/projects', as_list=True)
+ self.assertIsInstance(result, list)
+ self.assertEqual(len(result), 1)
+
+ with HTTMock(resp_cont):
+ result = self.gl.http_list('/projects', as_list=False)
+ self.assertIsInstance(result, GitlabList)
+ self.assertEqual(len(result), 1)
+
+ with HTTMock(resp_cont):
+ result = self.gl.http_list('/projects', all=True)
+ self.assertIsInstance(result, list)
+ self.assertEqual(len(result), 1)
+
+ def test_list_request_404(self):
+ @urlmatch(scheme="http", netloc="localhost",
+ path="/api/v4/not_there", method="get")
+ def resp_cont(url, request):
+ content = {'Here is why it failed'}
+ return response(404, content, {}, None, 5, request)
+
+ with HTTMock(resp_cont):
+ self.assertRaises(GitlabHttpError, self.gl.http_list, '/not_there')
+
+ def test_list_request_invalid_data(self):
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects",
+ method="get")
+ def resp_cont(url, request):
+ headers = {'content-type': 'application/json'}
+ content = '["name": "project1"]'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ self.assertRaises(GitlabParsingError, self.gl.http_list,
+ '/projects')
+
+ def test_post_request(self):
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects",
+ method="post")
+ def resp_cont(url, request):
+ headers = {'content-type': 'application/json'}
+ content = '{"name": "project1"}'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ result = self.gl.http_post('/projects')
+ self.assertIsInstance(result, dict)
+ self.assertEqual(result['name'], 'project1')
+
+ def test_post_request_404(self):
+ @urlmatch(scheme="http", netloc="localhost",
+ path="/api/v4/not_there", method="post")
+ def resp_cont(url, request):
+ content = {'Here is wh it failed'}
+ return response(404, content, {}, None, 5, request)
+
+ with HTTMock(resp_cont):
+ self.assertRaises(GitlabHttpError, self.gl.http_post, '/not_there')
+
+ def test_post_request_invalid_data(self):
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects",
+ method="post")
+ def resp_cont(url, request):
+ headers = {'content-type': 'application/json'}
+ content = '["name": "project1"]'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ self.assertRaises(GitlabParsingError, self.gl.http_post,
+ '/projects')
+
+ def test_put_request(self):
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects",
+ method="put")
+ def resp_cont(url, request):
+ headers = {'content-type': 'application/json'}
+ content = '{"name": "project1"}'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ result = self.gl.http_put('/projects')
+ self.assertIsInstance(result, dict)
+ self.assertEqual(result['name'], 'project1')
+
+ def test_put_request_404(self):
+ @urlmatch(scheme="http", netloc="localhost",
+ path="/api/v4/not_there", method="put")
+ def resp_cont(url, request):
+ content = {'Here is wh it failed'}
+ return response(404, content, {}, None, 5, request)
+
+ with HTTMock(resp_cont):
+ self.assertRaises(GitlabHttpError, self.gl.http_put, '/not_there')
+
+ def test_put_request_invalid_data(self):
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects",
+ method="put")
+ def resp_cont(url, request):
+ headers = {'content-type': 'application/json'}
+ content = '["name": "project1"]'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ self.assertRaises(GitlabParsingError, self.gl.http_put,
+ '/projects')
+
+ def test_delete_request(self):
+ @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects",
+ method="delete")
+ def resp_cont(url, request):
+ headers = {'content-type': 'application/json'}
+ content = 'true'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ result = self.gl.http_delete('/projects')
+ self.assertIsInstance(result, requests.Response)
+ self.assertEqual(result.json(), True)
+
+ def test_delete_request_404(self):
+ @urlmatch(scheme="http", netloc="localhost",
+ path="/api/v4/not_there", method="delete")
+ def resp_cont(url, request):
+ content = {'Here is wh it failed'}
+ return response(404, content, {}, None, 5, request)
+
+ with HTTMock(resp_cont):
+ self.assertRaises(GitlabHttpError, self.gl.http_delete,
+ '/not_there')
+
+
class TestGitlabMethods(unittest.TestCase):
def setUp(self):
self.gl = Gitlab("http://localhost", private_token="private_token",
diff --git a/gitlab/tests/test_gitlabobject.py b/gitlab/tests/test_gitlabobject.py
index 3bffb82..695f900 100644
--- a/gitlab/tests/test_gitlabobject.py
+++ b/gitlab/tests/test_gitlabobject.py
@@ -18,7 +18,6 @@
# 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 json
diff --git a/gitlab/tests/test_mixins.py b/gitlab/tests/test_mixins.py
new file mode 100644
index 0000000..812a118
--- /dev/null
+++ b/gitlab/tests/test_mixins.py
@@ -0,0 +1,411 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014 Mika Mäenpää <mika.j.maenpaa@tut.fi>,
+# Tampere University of Technology
+#
+# 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
+
+try:
+ import unittest
+except ImportError:
+ import unittest2 as unittest
+
+from httmock import HTTMock # noqa
+from httmock import response # noqa
+from httmock import urlmatch # noqa
+
+from gitlab import * # noqa
+from gitlab.base import * # noqa
+from gitlab.mixins import * # noqa
+
+
+class TestObjectMixinsAttributes(unittest.TestCase):
+ def test_access_request_mixin(self):
+ class O(AccessRequestMixin):
+ pass
+
+ obj = O()
+ self.assertTrue(hasattr(obj, 'approve'))
+
+ def test_subscribable_mixin(self):
+ class O(SubscribableMixin):
+ pass
+
+ obj = O()
+ self.assertTrue(hasattr(obj, 'subscribe'))
+ self.assertTrue(hasattr(obj, 'unsubscribe'))
+
+ def test_todo_mixin(self):
+ class O(TodoMixin):
+ pass
+
+ obj = O()
+ self.assertTrue(hasattr(obj, 'todo'))
+
+ def test_time_tracking_mixin(self):
+ class O(TimeTrackingMixin):
+ pass
+
+ obj = O()
+ self.assertTrue(hasattr(obj, 'time_stats'))
+ self.assertTrue(hasattr(obj, 'time_estimate'))
+ self.assertTrue(hasattr(obj, 'reset_time_estimate'))
+ self.assertTrue(hasattr(obj, 'add_spent_time'))
+ self.assertTrue(hasattr(obj, 'reset_spent_time'))
+
+
+class TestMetaMixins(unittest.TestCase):
+ def test_retrieve_mixin(self):
+ class M(RetrieveMixin):
+ pass
+
+ obj = M()
+ self.assertTrue(hasattr(obj, 'list'))
+ self.assertTrue(hasattr(obj, 'get'))
+ self.assertFalse(hasattr(obj, 'create'))
+ self.assertFalse(hasattr(obj, 'update'))
+ self.assertFalse(hasattr(obj, 'delete'))
+ self.assertIsInstance(obj, ListMixin)
+ self.assertIsInstance(obj, GetMixin)
+
+ def test_crud_mixin(self):
+ class M(CRUDMixin):
+ pass
+
+ obj = M()
+ self.assertTrue(hasattr(obj, 'get'))
+ self.assertTrue(hasattr(obj, 'list'))
+ self.assertTrue(hasattr(obj, 'create'))
+ self.assertTrue(hasattr(obj, 'update'))
+ self.assertTrue(hasattr(obj, 'delete'))
+ self.assertIsInstance(obj, ListMixin)
+ self.assertIsInstance(obj, GetMixin)
+ self.assertIsInstance(obj, CreateMixin)
+ self.assertIsInstance(obj, UpdateMixin)
+ self.assertIsInstance(obj, DeleteMixin)
+
+ def test_no_update_mixin(self):
+ class M(NoUpdateMixin):
+ pass
+
+ obj = M()
+ self.assertTrue(hasattr(obj, 'get'))
+ self.assertTrue(hasattr(obj, 'list'))
+ self.assertTrue(hasattr(obj, 'create'))
+ self.assertFalse(hasattr(obj, 'update'))
+ self.assertTrue(hasattr(obj, 'delete'))
+ self.assertIsInstance(obj, ListMixin)
+ self.assertIsInstance(obj, GetMixin)
+ self.assertIsInstance(obj, CreateMixin)
+ self.assertNotIsInstance(obj, UpdateMixin)
+ self.assertIsInstance(obj, DeleteMixin)
+
+
+class FakeObject(base.RESTObject):
+ pass
+
+
+class FakeManager(base.RESTManager):
+ _path = '/tests'
+ _obj_cls = FakeObject
+
+
+class TestMixinMethods(unittest.TestCase):
+ def setUp(self):
+ self.gl = Gitlab("http://localhost", private_token="private_token",
+ api_version=4)
+
+ def test_get_mixin(self):
+ class M(GetMixin, FakeManager):
+ pass
+
+ @urlmatch(scheme="http", netloc="localhost", path='/api/v4/tests/42',
+ method="get")
+ def resp_cont(url, request):
+ headers = {'Content-Type': 'application/json'}
+ content = '{"id": 42, "foo": "bar"}'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ mgr = M(self.gl)
+ obj = mgr.get(42)
+ self.assertIsInstance(obj, FakeObject)
+ self.assertEqual(obj.foo, 'bar')
+ self.assertEqual(obj.id, 42)
+
+ def test_get_without_id_mixin(self):
+ class M(GetWithoutIdMixin, FakeManager):
+ pass
+
+ @urlmatch(scheme="http", netloc="localhost", path='/api/v4/tests',
+ method="get")
+ def resp_cont(url, request):
+ headers = {'Content-Type': 'application/json'}
+ content = '{"foo": "bar"}'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ mgr = M(self.gl)
+ obj = mgr.get()
+ self.assertIsInstance(obj, FakeObject)
+ self.assertEqual(obj.foo, 'bar')
+ self.assertFalse(hasattr(obj, 'id'))
+
+ def test_list_mixin(self):
+ class M(ListMixin, FakeManager):
+ pass
+
+ @urlmatch(scheme="http", netloc="localhost", path='/api/v4/tests',
+ method="get")
+ def resp_cont(url, request):
+ headers = {'Content-Type': 'application/json'}
+ content = '[{"id": 42, "foo": "bar"},{"id": 43, "foo": "baz"}]'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ # test RESTObjectList
+ mgr = M(self.gl)
+ obj_list = mgr.list(as_list=False)
+ self.assertIsInstance(obj_list, base.RESTObjectList)
+ for obj in obj_list:
+ self.assertIsInstance(obj, FakeObject)
+ self.assertIn(obj.id, (42, 43))
+
+ # test list()
+ obj_list = mgr.list(all=True)
+ self.assertIsInstance(obj_list, list)
+ self.assertEqual(obj_list[0].id, 42)
+ self.assertEqual(obj_list[1].id, 43)
+ self.assertIsInstance(obj_list[0], FakeObject)
+ self.assertEqual(len(obj_list), 2)
+
+ def test_list_other_url(self):
+ class M(ListMixin, FakeManager):
+ pass
+
+ @urlmatch(scheme="http", netloc="localhost", path='/api/v4/others',
+ method="get")
+ def resp_cont(url, request):
+ headers = {'Content-Type': 'application/json'}
+ content = '[{"id": 42, "foo": "bar"}]'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ mgr = M(self.gl)
+ obj_list = mgr.list(path='/others', as_list=False)
+ self.assertIsInstance(obj_list, base.RESTObjectList)
+ obj = obj_list.next()
+ self.assertEqual(obj.id, 42)
+ self.assertEqual(obj.foo, 'bar')
+ self.assertRaises(StopIteration, obj_list.next)
+
+ def test_get_from_list_mixin(self):
+ class M(GetFromListMixin, FakeManager):
+ pass
+
+ @urlmatch(scheme="http", netloc="localhost", path='/api/v4/tests',
+ method="get")
+ def resp_cont(url, request):
+ headers = {'Content-Type': 'application/json'}
+ content = '[{"id": 42, "foo": "bar"},{"id": 43, "foo": "baz"}]'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ mgr = M(self.gl)
+ obj = mgr.get(42)
+ self.assertIsInstance(obj, FakeObject)
+ self.assertEqual(obj.foo, 'bar')
+ self.assertEqual(obj.id, 42)
+
+ self.assertRaises(GitlabGetError, mgr.get, 44)
+
+ def test_create_mixin_get_attrs(self):
+ class M1(CreateMixin, FakeManager):
+ pass
+
+ class M2(CreateMixin, FakeManager):
+ _create_attrs = (('foo',), ('bar', 'baz'))
+ _update_attrs = (('foo',), ('bam', ))
+
+ mgr = M1(self.gl)
+ required, optional = mgr.get_create_attrs()
+ self.assertEqual(len(required), 0)
+ self.assertEqual(len(optional), 0)
+
+ mgr = M2(self.gl)
+ required, optional = mgr.get_create_attrs()
+ self.assertIn('foo', required)
+ self.assertIn('bar', optional)
+ self.assertIn('baz', optional)
+ self.assertNotIn('bam', optional)
+
+ def test_create_mixin_missing_attrs(self):
+ class M(CreateMixin, FakeManager):
+ _create_attrs = (('foo',), ('bar', 'baz'))
+
+ mgr = M(self.gl)
+ data = {'foo': 'bar', 'baz': 'blah'}
+ mgr._check_missing_create_attrs(data)
+
+ data = {'baz': 'blah'}
+ with self.assertRaises(AttributeError) as error:
+ mgr._check_missing_create_attrs(data)
+ self.assertIn('foo', str(error.exception))
+
+ def test_create_mixin(self):
+ class M(CreateMixin, FakeManager):
+ _create_attrs = (('foo',), ('bar', 'baz'))
+ _update_attrs = (('foo',), ('bam', ))
+
+ @urlmatch(scheme="http", netloc="localhost", path='/api/v4/tests',
+ method="post")
+ def resp_cont(url, request):
+ headers = {'Content-Type': 'application/json'}
+ content = '{"id": 42, "foo": "bar"}'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ mgr = M(self.gl)
+ obj = mgr.create({'foo': 'bar'})
+ self.assertIsInstance(obj, FakeObject)
+ self.assertEqual(obj.id, 42)
+ self.assertEqual(obj.foo, 'bar')
+
+ def test_create_mixin_custom_path(self):
+ class M(CreateMixin, FakeManager):
+ _create_attrs = (('foo',), ('bar', 'baz'))
+ _update_attrs = (('foo',), ('bam', ))
+
+ @urlmatch(scheme="http", netloc="localhost", path='/api/v4/others',
+ method="post")
+ def resp_cont(url, request):
+ headers = {'Content-Type': 'application/json'}
+ content = '{"id": 42, "foo": "bar"}'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ mgr = M(self.gl)
+ obj = mgr.create({'foo': 'bar'}, path='/others')
+ self.assertIsInstance(obj, FakeObject)
+ self.assertEqual(obj.id, 42)
+ self.assertEqual(obj.foo, 'bar')
+
+ def test_update_mixin_get_attrs(self):
+ class M1(UpdateMixin, FakeManager):
+ pass
+
+ class M2(UpdateMixin, FakeManager):
+ _create_attrs = (('foo',), ('bar', 'baz'))
+ _update_attrs = (('foo',), ('bam', ))
+
+ mgr = M1(self.gl)
+ required, optional = mgr.get_update_attrs()
+ self.assertEqual(len(required), 0)
+ self.assertEqual(len(optional), 0)
+
+ mgr = M2(self.gl)
+ required, optional = mgr.get_update_attrs()
+ self.assertIn('foo', required)
+ self.assertIn('bam', optional)
+ self.assertNotIn('bar', optional)
+ self.assertNotIn('baz', optional)
+
+ def test_update_mixin_missing_attrs(self):
+ class M(UpdateMixin, FakeManager):
+ _update_attrs = (('foo',), ('bar', 'baz'))
+
+ mgr = M(self.gl)
+ data = {'foo': 'bar', 'baz': 'blah'}
+ mgr._check_missing_update_attrs(data)
+
+ data = {'baz': 'blah'}
+ with self.assertRaises(AttributeError) as error:
+ mgr._check_missing_update_attrs(data)
+ self.assertIn('foo', str(error.exception))
+
+ def test_update_mixin(self):
+ class M(UpdateMixin, FakeManager):
+ _create_attrs = (('foo',), ('bar', 'baz'))
+ _update_attrs = (('foo',), ('bam', ))
+
+ @urlmatch(scheme="http", netloc="localhost", path='/api/v4/tests/42',
+ method="put")
+ def resp_cont(url, request):
+ headers = {'Content-Type': 'application/json'}
+ content = '{"id": 42, "foo": "baz"}'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ mgr = M(self.gl)
+ server_data = mgr.update(42, {'foo': 'baz'})
+ self.assertIsInstance(server_data, dict)
+ self.assertEqual(server_data['id'], 42)
+ self.assertEqual(server_data['foo'], 'baz')
+
+ def test_update_mixin_no_id(self):
+ class M(UpdateMixin, FakeManager):
+ _create_attrs = (('foo',), ('bar', 'baz'))
+ _update_attrs = (('foo',), ('bam', ))
+
+ @urlmatch(scheme="http", netloc="localhost", path='/api/v4/tests',
+ method="put")
+ def resp_cont(url, request):
+ headers = {'Content-Type': 'application/json'}
+ content = '{"foo": "baz"}'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ mgr = M(self.gl)
+ server_data = mgr.update(new_data={'foo': 'baz'})
+ self.assertIsInstance(server_data, dict)
+ self.assertEqual(server_data['foo'], 'baz')
+
+ def test_delete_mixin(self):
+ class M(DeleteMixin, FakeManager):
+ pass
+
+ @urlmatch(scheme="http", netloc="localhost", path='/api/v4/tests/42',
+ method="delete")
+ def resp_cont(url, request):
+ headers = {'Content-Type': 'application/json'}
+ content = ''
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ mgr = M(self.gl)
+ mgr.delete(42)
+
+ def test_save_mixin(self):
+ class M(UpdateMixin, FakeManager):
+ pass
+
+ class O(SaveMixin, RESTObject):
+ pass
+
+ @urlmatch(scheme="http", netloc="localhost", path='/api/v4/tests/42',
+ method="put")
+ def resp_cont(url, request):
+ headers = {'Content-Type': 'application/json'}
+ content = '{"id": 42, "foo": "baz"}'
+ return response(200, content, headers, None, 5, request)
+
+ with HTTMock(resp_cont):
+ mgr = M(self.gl)
+ obj = O(mgr, {'id': 42, 'foo': 'bar'})
+ obj.foo = 'baz'
+ obj.save()
+ self.assertEqual(obj._attrs['foo'], 'baz')
+ self.assertDictEqual(obj._updated_attrs, {})
diff --git a/gitlab/v3/cli.py b/gitlab/v3/cli.py
new file mode 100644
index 0000000..b0450e8
--- /dev/null
+++ b/gitlab/v3/cli.py
@@ -0,0 +1,497 @@
+#!/usr/bin/env python
+# -*- 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 absolute_import
+import inspect
+import operator
+import sys
+
+import six
+
+import gitlab
+import gitlab.base
+from gitlab import cli
+import gitlab.v3.objects
+
+
+EXTRA_ACTIONS = {
+ gitlab.v3.objects.Group: {
+ 'search': {'required': ['query']}},
+ gitlab.v3.objects.ProjectBranch: {
+ 'protect': {'required': ['id', 'project-id']},
+ 'unprotect': {'required': ['id', 'project-id']}},
+ gitlab.v3.objects.ProjectBuild: {
+ 'cancel': {'required': ['id', 'project-id']},
+ 'retry': {'required': ['id', 'project-id']},
+ 'artifacts': {'required': ['id', 'project-id']},
+ 'trace': {'required': ['id', 'project-id']}},
+ gitlab.v3.objects.ProjectCommit: {
+ 'diff': {'required': ['id', 'project-id']},
+ 'blob': {'required': ['id', 'project-id', 'filepath']},
+ 'builds': {'required': ['id', 'project-id']},
+ 'cherrypick': {'required': ['id', 'project-id', 'branch']}},
+ gitlab.v3.objects.ProjectIssue: {
+ 'subscribe': {'required': ['id', 'project-id']},
+ 'unsubscribe': {'required': ['id', 'project-id']},
+ 'move': {'required': ['id', 'project-id', 'to-project-id']}},
+ gitlab.v3.objects.ProjectMergeRequest: {
+ 'closes-issues': {'required': ['id', 'project-id']},
+ 'cancel': {'required': ['id', 'project-id']},
+ 'merge': {'required': ['id', 'project-id'],
+ 'optional': ['merge-commit-message',
+ 'should-remove-source-branch',
+ 'merged-when-build-succeeds']}},
+ gitlab.v3.objects.ProjectMilestone: {
+ 'issues': {'required': ['id', 'project-id']}},
+ gitlab.v3.objects.Project: {
+ 'search': {'required': ['query']},
+ 'owned': {},
+ 'all': {'optional': [('all', bool)]},
+ 'starred': {},
+ 'star': {'required': ['id']},
+ 'unstar': {'required': ['id']},
+ 'archive': {'required': ['id']},
+ 'unarchive': {'required': ['id']},
+ 'share': {'required': ['id', 'group-id', 'group-access']}},
+ gitlab.v3.objects.User: {
+ 'block': {'required': ['id']},
+ 'unblock': {'required': ['id']},
+ 'search': {'required': ['query']},
+ 'get-by-username': {'required': ['query']}},
+}
+
+
+class GitlabCLI(object):
+ def _get_id(self, cls, args):
+ try:
+ id = args.pop(cls.idAttr)
+ except Exception:
+ cli.die("Missing --%s argument" % cls.idAttr.replace('_', '-'))
+
+ return id
+
+ def do_create(self, cls, gl, what, args):
+ if not cls.canCreate:
+ cli.die("%s objects can't be created" % what)
+
+ try:
+ o = cls.create(gl, args)
+ except Exception as e:
+ cli.die("Impossible to create object", e)
+
+ return o
+
+ def do_list(self, cls, gl, what, args):
+ if not cls.canList:
+ cli.die("%s objects can't be listed" % what)
+
+ try:
+ l = cls.list(gl, **args)
+ except Exception as e:
+ cli.die("Impossible to list objects", e)
+
+ return l
+
+ def do_get(self, cls, gl, what, args):
+ if cls.canGet is False:
+ cli.die("%s objects can't be retrieved" % what)
+
+ id = None
+ if cls not in [gitlab.v3.objects.CurrentUser] and cls.getRequiresId:
+ id = self._get_id(cls, args)
+
+ try:
+ o = cls.get(gl, id, **args)
+ except Exception as e:
+ cli.die("Impossible to get object", e)
+
+ return o
+
+ def do_delete(self, cls, gl, what, args):
+ if not cls.canDelete:
+ cli.die("%s objects can't be deleted" % what)
+
+ id = args.pop(cls.idAttr)
+ try:
+ gl.delete(cls, id, **args)
+ except Exception as e:
+ cli.die("Impossible to destroy object", e)
+
+ def do_update(self, cls, gl, what, args):
+ if not cls.canUpdate:
+ cli.die("%s objects can't be updated" % what)
+
+ o = self.do_get(cls, gl, what, args)
+ try:
+ for k, v in args.items():
+ o.__dict__[k] = v
+ o.save()
+ except Exception as e:
+ cli.die("Impossible to update object", e)
+
+ return o
+
+ def do_group_search(self, cls, gl, what, args):
+ try:
+ return gl.groups.search(args['query'])
+ except Exception as e:
+ cli.die("Impossible to search projects", e)
+
+ def do_project_search(self, cls, gl, what, args):
+ try:
+ return gl.projects.search(args['query'])
+ except Exception as e:
+ cli.die("Impossible to search projects", e)
+
+ def do_project_all(self, cls, gl, what, args):
+ try:
+ return gl.projects.all(all=args.get('all', False))
+ except Exception as e:
+ cli.die("Impossible to list all projects", e)
+
+ def do_project_starred(self, cls, gl, what, args):
+ try:
+ return gl.projects.starred()
+ except Exception as e:
+ cli.die("Impossible to list starred projects", e)
+
+ def do_project_owned(self, cls, gl, what, args):
+ try:
+ return gl.projects.owned()
+ except Exception as e:
+ cli.die("Impossible to list owned projects", e)
+
+ def do_project_star(self, cls, gl, what, args):
+ try:
+ o = self.do_get(cls, gl, what, args)
+ o.star()
+ except Exception as e:
+ cli.die("Impossible to star project", e)
+
+ def do_project_unstar(self, cls, gl, what, args):
+ try:
+ o = self.do_get(cls, gl, what, args)
+ o.unstar()
+ except Exception as e:
+ cli.die("Impossible to unstar project", e)
+
+ def do_project_archive(self, cls, gl, what, args):
+ try:
+ o = self.do_get(cls, gl, what, args)
+ o.archive_()
+ except Exception as e:
+ cli.die("Impossible to archive project", e)
+
+ def do_project_unarchive(self, cls, gl, what, args):
+ try:
+ o = self.do_get(cls, gl, what, args)
+ o.unarchive_()
+ except Exception as e:
+ cli.die("Impossible to unarchive project", e)
+
+ def do_project_share(self, cls, gl, what, args):
+ try:
+ o = self.do_get(cls, gl, what, args)
+ o.share(args['group_id'], args['group_access'])
+ except Exception as e:
+ cli.die("Impossible to share project", e)
+
+ def do_user_block(self, cls, gl, what, args):
+ try:
+ o = self.do_get(cls, gl, what, args)
+ o.block()
+ except Exception as e:
+ cli.die("Impossible to block user", e)
+
+ def do_user_unblock(self, cls, gl, what, args):
+ try:
+ o = self.do_get(cls, gl, what, args)
+ o.unblock()
+ except Exception as e:
+ cli.die("Impossible to block user", e)
+
+ def do_project_commit_diff(self, cls, gl, what, args):
+ try:
+ o = self.do_get(cls, gl, what, args)
+ return [x['diff'] for x in o.diff()]
+ except Exception as e:
+ cli.die("Impossible to get commit diff", e)
+
+ def do_project_commit_blob(self, cls, gl, what, args):
+ try:
+ o = self.do_get(cls, gl, what, args)
+ return o.blob(args['filepath'])
+ except Exception as e:
+ cli.die("Impossible to get commit blob", e)
+
+ def do_project_commit_builds(self, cls, gl, what, args):
+ try:
+ o = self.do_get(cls, gl, what, args)
+ return o.builds()
+ except Exception as e:
+ cli.die("Impossible to get commit builds", e)
+
+ def do_project_commit_cherrypick(self, cls, gl, what, args):
+ try:
+ o = self.do_get(cls, gl, what, args)
+ o.cherry_pick(branch=args['branch'])
+ except Exception as e:
+ cli.die("Impossible to cherry-pick commit", e)
+
+ def do_project_build_cancel(self, cls, gl, what, args):
+ try:
+ o = self.do_get(cls, gl, what, args)
+ return o.cancel()
+ except Exception as e:
+ cli.die("Impossible to cancel project build", e)
+
+ def do_project_build_retry(self, cls, gl, what, args):
+ try:
+ o = self.do_get(cls, gl, what, args)
+ return o.retry()
+ except Exception as e:
+ cli.die("Impossible to retry project build", e)
+
+ def do_project_build_artifacts(self, cls, gl, what, args):
+ try:
+ o = self.do_get(cls, gl, what, args)
+ return o.artifacts()
+ except Exception as e:
+ cli.die("Impossible to get project build artifacts", e)
+
+ def do_project_build_trace(self, cls, gl, what, args):
+ try:
+ o = self.do_get(cls, gl, what, args)
+ return o.trace()
+ except Exception as e:
+ cli.die("Impossible to get project build trace", e)
+
+ def do_project_issue_subscribe(self, cls, gl, what, args):
+ try:
+ o = self.do_get(cls, gl, what, args)
+ o.subscribe()
+ except Exception as e:
+ cli.die("Impossible to subscribe to issue", e)
+
+ def do_project_issue_unsubscribe(self, cls, gl, what, args):
+ try:
+ o = self.do_get(cls, gl, what, args)
+ o.unsubscribe()
+ except Exception as e:
+ cli.die("Impossible to subscribe to issue", e)
+
+ def do_project_issue_move(self, cls, gl, what, args):
+ try:
+ o = self.do_get(cls, gl, what, args)
+ o.move(args['to_project_id'])
+ except Exception as e:
+ cli.die("Impossible to move issue", e)
+
+ def do_project_merge_request_closesissues(self, cls, gl, what, args):
+ try:
+ o = self.do_get(cls, gl, what, args)
+ return o.closes_issues()
+ except Exception as e:
+ cli.die("Impossible to list issues closed by merge request", e)
+
+ def do_project_merge_request_cancel(self, cls, gl, what, args):
+ try:
+ o = self.do_get(cls, gl, what, args)
+ return o.cancel_merge_when_build_succeeds()
+ except Exception as e:
+ cli.die("Impossible to cancel merge request", e)
+
+ def do_project_merge_request_merge(self, cls, gl, what, args):
+ try:
+ o = self.do_get(cls, gl, what, args)
+ should_remove = args.get('should_remove_source_branch', False)
+ build_succeeds = args.get('merged_when_build_succeeds', False)
+ return o.merge(
+ merge_commit_message=args.get('merge_commit_message', ''),
+ should_remove_source_branch=should_remove,
+ merged_when_build_succeeds=build_succeeds)
+ except Exception as e:
+ cli.die("Impossible to validate merge request", e)
+
+ def do_project_milestone_issues(self, cls, gl, what, args):
+ try:
+ o = self.do_get(cls, gl, what, args)
+ return o.issues()
+ except Exception as e:
+ cli.die("Impossible to get milestone issues", e)
+
+ def do_user_search(self, cls, gl, what, args):
+ try:
+ return gl.users.search(args['query'])
+ except Exception as e:
+ cli.die("Impossible to search users", e)
+
+ def do_user_getbyusername(self, cls, gl, what, args):
+ try:
+ return gl.users.search(args['query'])
+ except Exception as e:
+ cli.die("Impossible to get user %s" % args['query'], e)
+
+
+def _populate_sub_parser_by_class(cls, sub_parser):
+ for action_name in ['list', 'get', 'create', 'update', 'delete']:
+ attr = 'can' + action_name.capitalize()
+ if not getattr(cls, attr):
+ continue
+ sub_parser_action = sub_parser.add_parser(action_name)
+ [sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
+ required=True)
+ for x in cls.requiredUrlAttrs]
+ sub_parser_action.add_argument("--sudo", required=False)
+
+ if action_name == "list":
+ [sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
+ required=True)
+ for x in cls.requiredListAttrs]
+ sub_parser_action.add_argument("--page", required=False)
+ sub_parser_action.add_argument("--per-page", required=False)
+ sub_parser_action.add_argument("--all", required=False,
+ action='store_true')
+
+ if action_name in ["get", "delete"]:
+ if cls not in [gitlab.v3.objects.CurrentUser]:
+ if cls.getRequiresId:
+ id_attr = cls.idAttr.replace('_', '-')
+ sub_parser_action.add_argument("--%s" % id_attr,
+ required=True)
+ [sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
+ required=True)
+ for x in cls.requiredGetAttrs if x != cls.idAttr]
+
+ if action_name == "get":
+ [sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
+ required=False)
+ for x in cls.optionalGetAttrs]
+
+ if action_name == "list":
+ [sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
+ required=False)
+ for x in cls.optionalListAttrs]
+
+ if action_name == "create":
+ [sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
+ required=True)
+ for x in cls.requiredCreateAttrs]
+ [sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
+ required=False)
+ for x in cls.optionalCreateAttrs]
+
+ if action_name == "update":
+ id_attr = cls.idAttr.replace('_', '-')
+ sub_parser_action.add_argument("--%s" % id_attr,
+ required=True)
+
+ attrs = (cls.requiredUpdateAttrs
+ if (cls.requiredUpdateAttrs or cls.optionalUpdateAttrs)
+ else cls.requiredCreateAttrs)
+ [sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
+ required=True)
+ for x in attrs if x != cls.idAttr]
+
+ attrs = (cls.optionalUpdateAttrs
+ if (cls.requiredUpdateAttrs or cls.optionalUpdateAttrs)
+ else cls.optionalCreateAttrs)
+ [sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
+ required=False)
+ for x in attrs]
+
+ if cls in EXTRA_ACTIONS:
+ def _add_arg(parser, required, data):
+ extra_args = {}
+ if isinstance(data, tuple):
+ if data[1] is bool:
+ extra_args = {'action': 'store_true'}
+ data = data[0]
+
+ parser.add_argument("--%s" % data, required=required, **extra_args)
+
+ for action_name in sorted(EXTRA_ACTIONS[cls]):
+ sub_parser_action = sub_parser.add_parser(action_name)
+ d = EXTRA_ACTIONS[cls][action_name]
+ [_add_arg(sub_parser_action, True, arg)
+ for arg in d.get('required', [])]
+ [_add_arg(sub_parser_action, False, arg)
+ for arg in d.get('optional', [])]
+
+
+def extend_parser(parser):
+ subparsers = parser.add_subparsers(title='object', dest='what',
+ help="Object to manipulate.")
+ subparsers.required = True
+
+ # populate argparse for all Gitlab Object
+ classes = []
+ for cls in gitlab.v3.objects.__dict__.values():
+ try:
+ if gitlab.base.GitlabObject in inspect.getmro(cls):
+ classes.append(cls)
+ except AttributeError:
+ pass
+ classes.sort(key=operator.attrgetter("__name__"))
+
+ for cls in classes:
+ arg_name = cli.cls_to_what(cls)
+ object_group = subparsers.add_parser(arg_name)
+
+ object_subparsers = object_group.add_subparsers(
+ dest='action', help="Action to execute.")
+ _populate_sub_parser_by_class(cls, object_subparsers)
+ object_subparsers.required = True
+
+ return parser
+
+
+def run(gl, what, action, args, verbose):
+ try:
+ cls = gitlab.v3.objects.__dict__[cli.what_to_cls(what)]
+ except ImportError:
+ cli.die("Unknown object: %s" % what)
+
+ g_cli = GitlabCLI()
+ method = None
+ what = what.replace('-', '_')
+ action = action.lower().replace('-', '')
+ for test in ["do_%s_%s" % (what, action),
+ "do_%s" % action]:
+ if hasattr(g_cli, test):
+ method = test
+ break
+
+ if method is None:
+ sys.stderr.write("Don't know how to deal with this!\n")
+ sys.exit(1)
+
+ ret_val = getattr(g_cli, method)(cls, gl, what, args)
+
+ if isinstance(ret_val, list):
+ for o in ret_val:
+ if isinstance(o, gitlab.GitlabObject):
+ o.display(verbose)
+ print("")
+ else:
+ print(o)
+ elif isinstance(ret_val, gitlab.base.GitlabObject):
+ ret_val.display(verbose)
+ elif isinstance(ret_val, six.string_types):
+ print(ret_val)
diff --git a/gitlab/v3/objects.py b/gitlab/v3/objects.py
index 65015fc..69a9721 100644
--- a/gitlab/v3/objects.py
+++ b/gitlab/v3/objects.py
@@ -16,7 +16,6 @@
# 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
diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py
index 89321c9..49ccc9d 100644
--- a/gitlab/v4/objects.py
+++ b/gitlab/v4/objects.py
@@ -16,17 +16,14 @@
# 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 six
-from six.moves import urllib
-import gitlab
from gitlab.base import * # noqa
from gitlab.exceptions import * # noqa
+from gitlab.mixins import * # noqa
from gitlab import utils
VISIBILITY_PRIVATE = 'private'
@@ -40,587 +37,623 @@ ACCESS_MASTER = 40
ACCESS_OWNER = 50
-class SidekiqManager(object):
+class SidekiqManager(RESTManager):
"""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.
+
+ @exc.on_http_error(exc.GitlabGetError)
+ def queue_metrics(self, **kwargs):
+ """Return the registred queues information.
Args:
- gl (gitlab.Gitlab): Gitlab object referencing the GitLab server.
- """
- self.gitlab = gl
+ **kwargs: Extra options to send to the server (e.g. sudo)
- def _simple_get(self, url, **kwargs):
- r = self.gitlab._raw_get(url, **kwargs)
- raise_error_from_response(r, GitlabGetError)
- return r.json()
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabGetError: If the information couldn't be retrieved
- def queue_metrics(self, **kwargs):
- """Returns the registred queues information."""
- return self._simple_get('/sidekiq/queue_metrics', **kwargs)
+ Returns:
+ dict: Information about the Sidekiq queues
+ """
+ return self.gitlab.http_get('/sidekiq/queue_metrics', **kwargs)
+ @exc.on_http_error(exc.GitlabGetError)
def process_metrics(self, **kwargs):
- """Returns the registred sidekiq workers."""
- return self._simple_get('/sidekiq/process_metrics', **kwargs)
+ """Return the registred sidekiq workers.
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabGetError: If the information couldn't be retrieved
+
+ Returns:
+ dict: Information about the register Sidekiq worker
+ """
+ return self.gitlab.http_get('/sidekiq/process_metrics', **kwargs)
+
+ @exc.on_http_error(exc.GitlabGetError)
def job_stats(self, **kwargs):
- """Returns statistics about the jobs performed."""
- return self._simple_get('/sidekiq/job_stats', **kwargs)
+ """Return statistics about the jobs performed.
+
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabGetError: If the information couldn't be retrieved
+
+ Returns:
+ dict: Statistics about the Sidekiq jobs performed
+ """
+ return self.gitlab.http_get('/sidekiq/job_stats', **kwargs)
+ @exc.on_http_error(exc.GitlabGetError)
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')]),
+ """Return all available metrics and statistics.
+
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabGetError: If the information couldn't be retrieved
+
+ Returns:
+ dict: All available Sidekiq metrics and statistics
+ """
+ return self.gitlab.http_get('/sidekiq/compound_metrics', **kwargs)
+
+
+class UserEmail(ObjectDeleteMixin, RESTObject):
+ _short_print_attr = 'email'
+
+
+class UserEmailManager(RetrieveMixin, CreateMixin, DeleteMixin, RESTManager):
+ _path = '/users/%(user_id)s/emails'
+ _obj_cls = UserEmail
+ _from_parent_attrs = {'user_id': 'id'}
+ _create_attrs = (('email', ), tuple())
+
+
+class UserKey(ObjectDeleteMixin, RESTObject):
+ pass
+
+
+class UserKeyManager(GetFromListMixin, CreateMixin, DeleteMixin, RESTManager):
+ _path = '/users/%(user_id)s/keys'
+ _obj_cls = UserKey
+ _from_parent_attrs = {'user_id': 'id'}
+ _create_attrs = (('title', 'key'), tuple())
+
+
+class UserProject(RESTObject):
+ _constructor_types = {'owner': 'User', 'namespace': 'Group'}
+
+
+class UserProjectManager(CreateMixin, RESTManager):
+ _path = '/projects/user/%(user_id)s'
+ _obj_cls = UserProject
+ _from_parent_attrs = {'user_id': 'id'}
+ _create_attrs = (
+ ('name', ),
+ ('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')
)
- 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)
+class User(SaveMixin, ObjectDeleteMixin, RESTObject):
+ _short_print_attr = 'username'
+ _managers = (
+ ('emails', 'UserEmailManager'),
+ ('keys', 'UserKeyManager'),
+ ('projects', 'UserProjectManager'),
+ )
+
+ @exc.on_http_error(exc.GitlabBlockError)
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'
+ """Block the user.
+
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabBlockError: If the user could not be blocked
+
+ Returns:
+ bool: Whether the user status has been changed
+ """
+ path = '/users/%s/block' % self.id
+ server_data = self.manager.gitlab.http_post(path, **kwargs)
+ if server_data is True:
+ self._attrs['state'] = 'blocked'
+ return server_data
+ @exc.on_http_error(exc.GitlabUnblockError)
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'
+ """Unblock the user.
- 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
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabUnblockError: If the user could not be unblocked
+ Returns:
+ bool: Whether the user status has been changed
+ """
+ path = '/users/%s/unblock' % self.id
+ server_data = self.manager.gitlab.http_post(path, **kwargs)
+ if server_data is True:
+ self._attrs['state'] = 'active'
+ return server_data
+
+
+class UserManager(CRUDMixin, RESTManager):
+ _path = '/users'
+ _obj_cls = User
+
+ _list_filters = ('active', 'blocked', 'username', 'extern_uid', 'provider',
+ 'external')
+ _create_attrs = (
+ ('email', 'username', 'name'),
+ ('password', 'reset_password', 'skype', 'linkedin', 'twitter',
+ 'projects_limit', 'extern_uid', 'provider', 'bio', 'admin',
+ 'can_create_group', 'website_url', 'skip_confirmation', 'external',
+ 'organization', 'location')
+ )
+ _update_attrs = (
+ ('email', 'username', 'name'),
+ ('password', 'skype', 'linkedin', 'twitter', 'projects_limit',
+ 'extern_uid', 'provider', 'bio', 'admin', 'can_create_group',
+ 'website_url', 'skip_confirmation', 'external', 'organization',
+ 'location')
+ )
-class UserManager(BaseManager):
- obj_cls = User
+ def _sanitize_data(self, data, action):
+ new_data = data.copy()
+ if 'confirm' in data:
+ new_data['confirm'] = str(new_data['confirm']).lower()
+ return new_data
-class CurrentUserEmail(GitlabObject):
- _url = '/user/emails'
- canUpdate = False
- shortPrintAttr = 'email'
- requiredCreateAttrs = ['email']
+class CurrentUserEmail(ObjectDeleteMixin, RESTObject):
+ _short_print_attr = 'email'
-class CurrentUserEmailManager(BaseManager):
- obj_cls = CurrentUserEmail
+class CurrentUserEmailManager(RetrieveMixin, CreateMixin, DeleteMixin,
+ RESTManager):
+ _path = '/user/emails'
+ _obj_cls = CurrentUserEmail
+ _create_attrs = (('email', ), tuple())
-class CurrentUserKey(GitlabObject):
- _url = '/user/keys'
- canUpdate = False
- shortPrintAttr = 'title'
- requiredCreateAttrs = ['title', 'key']
+class CurrentUserKey(ObjectDeleteMixin, RESTObject):
+ _short_print_attr = 'title'
-class CurrentUserKeyManager(BaseManager):
- obj_cls = CurrentUserKey
+class CurrentUserKeyManager(RetrieveMixin, CreateMixin, DeleteMixin,
+ RESTManager):
+ _path = '/user/keys'
+ _obj_cls = CurrentUserKey
+ _create_attrs = (('title', 'key'), tuple())
-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 CurrentUser(RESTObject):
+ _id_attr = None
+ _short_print_attr = 'username'
+ _managers = (
+ ('emails', 'CurrentUserEmailManager'),
+ ('keys', 'CurrentUserKeyManager'),
)
-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 Dockerfile(GitlabObject):
- _url = '/templates/dockerfiles'
- canDelete = False
- canUpdate = False
- canCreate = False
- idAttr = 'name'
-
-
-class DockerfileManager(BaseManager):
- obj_cls = Dockerfile
-
-
-class Gitignore(GitlabObject):
- _url = '/templates/gitignores'
- canDelete = False
- canUpdate = False
- canCreate = False
- idAttr = 'name'
+class CurrentUserManager(GetWithoutIdMixin, RESTManager):
+ _path = '/user'
+ _obj_cls = CurrentUser
+ def credentials_auth(self, email, password):
+ data = {'email': email, 'password': password}
+ server_data = self.gitlab.http_post('/session', post_data=data)
+ return CurrentUser(self, server_data)
-class GitignoreManager(BaseManager):
- obj_cls = Gitignore
+class ApplicationSettings(SaveMixin, RESTObject):
+ _id_attr = None
-class Gitlabciyml(GitlabObject):
- _url = '/templates/gitlab_ci_ymls'
- canDelete = False
- canUpdate = False
- canCreate = False
- idAttr = 'name'
+class ApplicationSettingsManager(GetWithoutIdMixin, UpdateMixin, RESTManager):
+ _path = '/application/settings'
+ _obj_cls = ApplicationSettings
+ _update_attrs = (
+ tuple(),
+ ('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')
+ )
-class GitlabciymlManager(BaseManager):
- obj_cls = Gitlabciyml
+ def _sanitize_data(self, data, action):
+ new_data = data.copy()
+ if 'domain_whitelist' in data and data['domain_whitelist'] is None:
+ new_data.pop('domain_whitelist')
+ return new_data
-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 BroadcastMessage(SaveMixin, ObjectDeleteMixin, RESTObject):
+ pass
-class GroupIssueManager(BaseManager):
- obj_cls = GroupIssue
+class BroadcastMessageManager(CRUDMixin, RESTManager):
+ _path = '/broadcast_messages'
+ _obj_cls = BroadcastMessage
+ _create_attrs = (('message', ), ('starts_at', 'ends_at', 'color', 'font'))
+ _update_attrs = (tuple(), ('message', 'starts_at', 'ends_at', 'color',
+ 'font'))
-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']
- optionalUpdateAttrs = ['expires_at']
- shortPrintAttr = 'username'
- def _update(self, **kwargs):
- self.user_id = self.id
- super(GroupMember, self)._update(**kwargs)
+class DeployKey(RESTObject):
+ pass
-class GroupMemberManager(BaseManager):
- obj_cls = GroupMember
+class DeployKeyManager(GetFromListMixin, RESTManager):
+ _path = '/deploy_keys'
+ _obj_cls = DeployKey
-class GroupNotificationSettings(NotificationSettings):
- _url = '/groups/%(group_id)s/notification_settings'
- requiredUrlAttrs = ['group_id']
+class NotificationSettings(SaveMixin, RESTObject):
+ _id_attr = None
-class GroupNotificationSettingsManager(BaseManager):
- obj_cls = GroupNotificationSettings
+class NotificationSettingsManager(GetWithoutIdMixin, UpdateMixin, RESTManager):
+ _path = '/notification_settings'
+ _obj_cls = NotificationSettings
+ _update_attrs = (
+ tuple(),
+ ('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')
+ )
-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.
+class Dockerfile(RESTObject):
+ _id_attr = 'name'
- 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.
- """
+class DockerfileManager(RetrieveMixin, RESTManager):
+ _path = '/templates/dockerfiles'
+ _obj_cls = Dockerfile
+
+
+class Gitignore(RESTObject):
+ _id_attr = 'name'
+
+
+class GitignoreManager(RetrieveMixin, RESTManager):
+ _path = '/templates/gitignores'
+ _obj_cls = Gitignore
+
+
+class Gitlabciyml(RESTObject):
+ _id_attr = 'name'
+
+
+class GitlabciymlManager(RetrieveMixin, RESTManager):
+ _path = '/templates/gitlab_ci_ymls'
+ _obj_cls = Gitlabciyml
+
+
+class GroupIssue(RESTObject):
+ pass
+
+
+class GroupIssueManager(GetFromListMixin, RESTManager):
+ _path = '/groups/%(group_id)s/issues'
+ _obj_cls = GroupIssue
+ _from_parent_attrs = {'group_id': 'id'}
+ _list_filters = ('state', 'labels', 'milestone', 'order_by', 'sort')
- 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 GroupMember(SaveMixin, ObjectDeleteMixin, RESTObject):
+ _short_print_attr = 'username'
-class GroupAccessRequestManager(BaseManager):
- obj_cls = GroupAccessRequest
+class GroupMemberManager(GetFromListMixin, CreateMixin, UpdateMixin,
+ DeleteMixin, RESTManager):
+ _path = '/groups/%(group_id)s/members'
+ _obj_cls = GroupMember
+ _from_parent_attrs = {'group_id': 'id'}
+ _create_attrs = (('access_level', 'user_id'), ('expires_at', ))
+ _update_attrs = (('access_level', ), ('expires_at', ))
-class Hook(GitlabObject):
+
+class GroupNotificationSettings(NotificationSettings):
+ pass
+
+
+class GroupNotificationSettingsManager(NotificationSettingsManager):
+ _path = '/groups/%(group_id)s/notification_settings'
+ _obj_cls = GroupNotificationSettings
+ _from_parent_attrs = {'group_id': 'id'}
+
+
+class GroupAccessRequest(AccessRequestMixin, ObjectDeleteMixin, RESTObject):
+ pass
+
+
+class GroupAccessRequestManager(GetFromListMixin, CreateMixin, DeleteMixin,
+ RESTManager):
+ _path = '/groups/%(group_id)s/access_requests'
+ _obj_cls = GroupAccessRequest
+ _from_parent_attrs = {'group_id': 'id'}
+
+
+class Hook(ObjectDeleteMixin, RESTObject):
_url = '/hooks'
- canUpdate = False
- requiredCreateAttrs = ['url']
- shortPrintAttr = 'url'
+ _short_print_attr = 'url'
-class HookManager(BaseManager):
- obj_cls = Hook
+class HookManager(NoUpdateMixin, RESTManager):
+ _path = '/hooks'
+ _obj_cls = Hook
+ _create_attrs = (('url', ), tuple())
-class Issue(GitlabObject):
+class Issue(RESTObject):
_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']
-
+ _constructor_types = {'author': 'User',
+ 'assignee': 'User',
+ 'milestone': 'ProjectMilestone'}
+ _short_print_attr = 'title'
-class IssueManager(BaseManager):
- obj_cls = Issue
+class IssueManager(GetFromListMixin, RESTManager):
+ _path = '/issues'
+ _obj_cls = Issue
+ _list_filters = ('state', 'labels', 'order_by', 'sort')
-class License(GitlabObject):
- _url = '/templates/licenses'
- canDelete = False
- canUpdate = False
- canCreate = False
- idAttr = 'key'
- optionalListAttrs = ['popular']
- optionalGetAttrs = ['project', 'fullname']
+class License(RESTObject):
+ _id_attr = 'key'
-class LicenseManager(BaseManager):
- obj_cls = License
+class LicenseManager(RetrieveMixin, RESTManager):
+ _path = '/templates/licenses'
+ _obj_cls = License
+ _list_filters = ('popular', )
+ _optional_get_attrs = ('project', 'fullname')
-class Snippet(GitlabObject):
- _url = '/snippets'
- _constructorTypes = {'author': 'User'}
- requiredCreateAttrs = ['title', 'file_name', 'content']
- optionalCreateAttrs = ['lifetime', 'visibility']
- optionalUpdateAttrs = ['title', 'file_name', 'content', 'visibility']
- shortPrintAttr = 'title'
+class Snippet(SaveMixin, ObjectDeleteMixin, RESTObject):
+ _constructor_types = {'author': 'User'}
+ _short_print_attr = 'title'
- def raw(self, streamed=False, action=None, chunk_size=1024, **kwargs):
- """Return the raw content of a snippet.
+ @exc.on_http_error(exc.GitlabGetError)
+ def content(self, streamed=False, action=None, chunk_size=1024, **kwargs):
+ """Return the 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.
+ data
+ chunk_size (int): Size of each chunk
+ **kwargs: Extra options to send to the server (e.g. sudo)
Raises:
- GitlabConnectionError: If the server cannot be reached.
- GitlabGetError: If the server fails to perform the request.
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabGetError: If the content could not be retrieved
+
+ Returns:
+ str: The snippet content
"""
- 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)
+ path = '/snippets/%s/raw' % self.get_id()
+ result = self.manager.gitlab.http_get(path, streamed=streamed,
+ **kwargs)
+ return utils.response_content(result, streamed, action, chunk_size)
-class SnippetManager(BaseManager):
- obj_cls = Snippet
+class SnippetManager(CRUDMixin, RESTManager):
+ _path = '/snippets'
+ _obj_cls = Snippet
+ _create_attrs = (('title', 'file_name', 'content'),
+ ('lifetime', 'visibility'))
+ _update_attrs = (tuple(),
+ ('title', 'file_name', 'content', 'visibility'))
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.
+ all (bool): If True the returned object will be a list
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ Raises:
+ GitlabListError: If the list could not be retrieved
Returns:
- list(gitlab.Gitlab.Snippet): The list of snippets.
+ RESTObjectList: A generator for the snippets list
"""
- return self.gitlab._raw_list("/snippets/public", Snippet, **kwargs)
+ return self.list(path='/snippets/public', **kwargs)
-class Namespace(GitlabObject):
- _url = '/namespaces'
- canGet = 'from_list'
- canUpdate = False
- canDelete = False
- canCreate = False
- optionalListAttrs = ['search']
+class Namespace(RESTObject):
+ pass
-class NamespaceManager(BaseManager):
- obj_cls = Namespace
+class NamespaceManager(GetFromListMixin, RESTManager):
+ _path = '/namespaces'
+ _obj_cls = Namespace
+ _list_filters = ('search', )
-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 ProjectBoardList(SaveMixin, ObjectDeleteMixin, RESTObject):
+ _constructor_types = {'label': 'ProjectLabel'}
-class ProjectBoardListManager(BaseManager):
- obj_cls = ProjectBoardList
+class ProjectBoardListManager(CRUDMixin, RESTManager):
+ _path = '/projects/%(project_id)s/boards/%(board_id)s/lists'
+ _obj_cls = ProjectBoardList
+ _from_parent_attrs = {'project_id': 'project_id',
+ 'board_id': 'id'}
+ _create_attrs = (('label_id', ), tuple())
+ _update_attrs = (('position', ), tuple())
-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 ProjectBoard(RESTObject):
+ _constructor_types = {'labels': 'ProjectBoardList'}
+ _managers = (('lists', 'ProjectBoardListManager'), )
-class ProjectBoardManager(BaseManager):
- obj_cls = ProjectBoard
+class ProjectBoardManager(GetFromListMixin, RESTManager):
+ _path = '/projects/%(project_id)s/boards'
+ _obj_cls = ProjectBoard
+ _from_parent_attrs = {'project_id': 'id'}
-class ProjectBranch(GitlabObject):
- _url = '/projects/%(project_id)s/repository/branches'
- _constructorTypes = {'author': 'User', "committer": "User"}
+class ProjectBranch(ObjectDeleteMixin, RESTObject):
+ _constructor_types = {'author': 'User', "committer": "User"}
+ _id_attr = 'name'
- idAttr = 'name'
- canUpdate = False
- requiredUrlAttrs = ['project_id']
- requiredCreateAttrs = ['branch', 'ref']
+ @exc.on_http_error(exc.GitlabProtectError)
+ def protect(self, developers_can_push=False, developers_can_merge=False,
+ **kwargs):
+ """Protect the branch.
- 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)
+ Args:
+ developers_can_push (bool): Set to True if developers are allowed
+ to push to the branch
+ developers_can_merge (bool): Set to True if developers are allowed
+ to merge to the branch
+ **kwargs: Extra options to send to the server (e.g. sudo)
- if protect:
- self.protected = protect
- else:
- del self.protected
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabProtectError: If the branch could not be protected
+ """
+ path = '%s/%s/protect' % (self.manager.path, self.get_id())
+ post_data = {'developers_can_push': developers_can_push,
+ 'developers_can_merge': developers_can_merge}
+ self.manager.gitlab.http_put(path, post_data=post_data, **kwargs)
+ self._attrs['protected'] = True
+ @exc.on_http_error(exc.GitlabProtectError)
def unprotect(self, **kwargs):
- """Unprotects the branch."""
- self.protect(False, **kwargs)
+ """Unprotect the branch.
+
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabProtectError: If the branch could not be unprotected
+ """
+ path = '%s/%s/protect' % (self.manager.path, self.get_id())
+ self.manager.gitlab.http_put(path, **kwargs)
+ self._attrs['protected'] = False
-class ProjectBranchManager(BaseManager):
- obj_cls = ProjectBranch
+class ProjectBranchManager(NoUpdateMixin, RESTManager):
+ _path = '/projects/%(project_id)s/repository/branches'
+ _obj_cls = ProjectBranch
+ _from_parent_attrs = {'project_id': 'id'}
+ _create_attrs = (('branch', 'ref'), tuple())
-class ProjectJob(GitlabObject):
- _url = '/projects/%(project_id)s/jobs'
- _constructorTypes = {'user': 'User',
- 'commit': 'ProjectCommit',
- 'runner': 'Runner'}
- requiredUrlAttrs = ['project_id']
- canDelete = False
- canUpdate = False
- canCreate = False
+class ProjectJob(RESTObject):
+ _constructor_types = {'user': 'User',
+ 'commit': 'ProjectCommit',
+ 'runner': 'Runner'}
+ @exc.on_http_error(exc.GitlabJobCancelError)
def cancel(self, **kwargs):
- """Cancel the job."""
- url = '/projects/%s/jobs/%s/cancel' % (self.project_id, self.id)
- r = self.gitlab._raw_post(url)
- raise_error_from_response(r, GitlabJobCancelError, 201)
+ """Cancel the job.
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabJobCancelError: If the job could not be canceled
+ """
+ path = '%s/%s/cancel' % (self.manager.path, self.get_id())
+ self.manager.gitlab.http_post(path)
+
+ @exc.on_http_error(exc.GitlabJobRetryError)
def retry(self, **kwargs):
- """Retry the job."""
- url = '/projects/%s/jobs/%s/retry' % (self.project_id, self.id)
- r = self.gitlab._raw_post(url)
- raise_error_from_response(r, GitlabJobRetryError, 201)
+ """Retry the job.
+
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabJobRetryError: If the job could not be retried
+ """
+ path = '%s/%s/retry' % (self.manager.path, self.get_id())
+ self.manager.gitlab.http_post(path)
+ @exc.on_http_error(exc.GitlabJobPlayError)
def play(self, **kwargs):
- """Trigger a job explicitly."""
- url = '/projects/%s/jobs/%s/play' % (self.project_id, self.id)
- r = self.gitlab._raw_post(url)
- raise_error_from_response(r, GitlabJobPlayError)
+ """Trigger a job explicitly.
+
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabJobPlayError: If the job could not be triggered
+ """
+ path = '%s/%s/play' % (self.manager.path, self.get_id())
+ self.manager.gitlab.http_post(path)
+
+ @exc.on_http_error(exc.GitlabJobEraseError)
def erase(self, **kwargs):
- """Erase the job (remove job artifacts and trace)."""
- url = '/projects/%s/jobs/%s/erase' % (self.project_id, self.id)
- r = self.gitlab._raw_post(url)
- raise_error_from_response(r, GitlabJobEraseError, 201)
+ """Erase the job (remove job artifacts and trace).
+
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabJobEraseError: If the job could not be erased
+ """
+ path = '%s/%s/erase' % (self.manager.path, self.get_id())
+ self.manager.gitlab.http_post(path)
+ @exc.on_http_error(exc.GitlabCreateError)
def keep_artifacts(self, **kwargs):
- """Prevent artifacts from being delete when expiration is set.
+ """Prevent artifacts from being deleted when expiration is set.
+
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
Raises:
- GitlabConnectionError: If the server cannot be reached.
- GitlabCreateError: If the request failed.
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabCreateError: If the request could not be performed
"""
- url = ('/projects/%s/jobs/%s/artifacts/keep' %
- (self.project_id, self.id))
- r = self.gitlab._raw_post(url)
- raise_error_from_response(r, GitlabGetError, 200)
+ path = '%s/%s/artifacts/keep' % (self.manager.path, self.get_id())
+ self.manager.gitlab.http_post(path)
+ @exc.on_http_error(exc.GitlabGetError)
def artifacts(self, streamed=False, action=None, chunk_size=1024,
**kwargs):
"""Get the job artifacts.
@@ -628,447 +661,325 @@ class ProjectJob(GitlabObject):
Args:
streamed (bool): If True the data will be processed by chunks of
`chunk_size` and each chunk is passed to `action` for
- treatment.
+ treatment
action (callable): Callable responsible of dealing with chunk of
- data.
- chunk_size (int): Size of each chunk.
+ data
+ chunk_size (int): Size of each chunk
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabGetError: If the artifacts could not be retrieved
Returns:
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/jobs/%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)
+ path = '%s/%s/artifacts' % (self.manager.path, self.get_id())
+ result = self.manager.gitlab.get_http(path, streamed=streamed,
+ **kwargs)
+ return utils.response_content(result, streamed, action, chunk_size)
+ @exc.on_http_error(exc.GitlabGetError)
def trace(self, streamed=False, action=None, chunk_size=1024, **kwargs):
"""Get the job 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.
+ treatment
action (callable): Callable responsible of dealing with chunk of
- data.
- chunk_size (int): Size of each chunk.
-
- Returns:
- str: The trace.
+ data
+ chunk_size (int): Size of each chunk
+ **kwargs: Extra options to send to the server (e.g. sudo)
Raises:
- GitlabConnectionError: If the server cannot be reached.
- GitlabGetError: If the trace is not available.
- """
- url = '/projects/%s/jobs/%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 ProjectJobManager(BaseManager):
- obj_cls = ProjectJob
-
-
-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.
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabGetError: If the artifacts could not be retrieved
Returns:
- str: The content of the file
-
- Raises:
- GitlabConnectionError: If the server cannot be reached.
- GitlabGetError: If the server fails to perform the request.
+ str: The trace
"""
- 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)
+ path = '%s/%s/trace' % (self.manager.path, self.get_id())
+ result = self.manager.gitlab.get_http(path, streamed=streamed,
+ **kwargs)
+ return utils.response_content(result, streamed, action, chunk_size)
- def cherry_pick(self, branch, **kwargs):
- """Cherry-pick a commit into a branch.
- Args:
- branch (str): Name of target branch.
+class ProjectJobManager(RetrieveMixin, RESTManager):
+ _path = '/projects/%(project_id)s/jobs'
+ _obj_cls = ProjectJob
+ _from_parent_attrs = {'project_id': 'id'}
- 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 ProjectCommitStatus(RESTObject):
+ pass
-class ProjectCommitManager(BaseManager):
- obj_cls = ProjectCommit
+class ProjectCommitStatusManager(RetrieveMixin, CreateMixin, RESTManager):
+ _path = ('/projects/%(project_id)s/repository/commits/%(commit_id)s'
+ '/statuses')
+ _obj_cls = ProjectCommitStatus
+ _from_parent_attrs = {'project_id': 'project_id', 'commit_id': 'id'}
+ _create_attrs = (('state', ),
+ ('description', 'name', 'context', 'ref', 'target_url'))
+ def create(self, data, **kwargs):
+ """Create a new object.
-class ProjectEnvironment(GitlabObject):
- _url = '/projects/%(project_id)s/environments'
- canGet = 'from_list'
- requiredUrlAttrs = ['project_id']
- requiredCreateAttrs = ['name']
- optionalCreateAttrs = ['external_url']
- optionalUpdateAttrs = ['name', 'external_url']
+ Args:
+ data (dict): Parameters to send to the server to create the
+ resource
+ **kwargs: Extra data to send to the Gitlab server (e.g. sudo or
+ 'ref_name', 'stage', 'name', 'all'.
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabCreateError: If the server cannot perform the request
+ Returns:
+ RESTObject: A new instance of the manage object class build with
+ the data sent by the server
+ """
+ path = '/projects/%(project_id)s/statuses/%(commit_id)s'
+ computed_path = self._compute_path(path)
+ return CreateMixin.create(self, data, path=computed_path, **kwargs)
-class ProjectEnvironmentManager(BaseManager):
- obj_cls = ProjectEnvironment
+class ProjectCommitComment(RESTObject):
+ pass
-class ProjectKey(GitlabObject):
- _url = '/projects/%(project_id)s/deploy_keys'
- canUpdate = False
- requiredUrlAttrs = ['project_id']
- requiredCreateAttrs = ['title', 'key']
+class ProjectCommitCommentManager(ListMixin, CreateMixin, RESTManager):
+ _path = ('/projects/%(project_id)s/repository/commits/%(commit_id)s'
+ '/comments')
+ _obj_cls = ProjectCommitComment
+ _from_parent_attrs = {'project_id': 'project_id', 'commit_id': 'id'}
+ _create_attrs = (('note', ), ('path', 'line', 'line_type'))
-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 ProjectCommit(RESTObject):
+ _short_print_attr = 'title'
+ _managers = (
+ ('comments', 'ProjectCommitCommentManager'),
+ ('statuses', 'ProjectCommitStatusManager'),
+ )
+ @exc.on_http_error(exc.GitlabGetError)
+ def diff(self, **kwargs):
+ """Generate the commit diff.
-class ProjectEvent(GitlabObject):
- _url = '/projects/%(project_id)s/events'
- canGet = 'from_list'
- canDelete = False
- canUpdate = False
- canCreate = False
- requiredUrlAttrs = ['project_id']
- shortPrintAttr = 'target_title'
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
+ Raise:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabGetError: If the diff could not be retrieved
-class ProjectEventManager(BaseManager):
- obj_cls = ProjectEvent
+ Returns:
+ list: The changes done in this commit
+ """
+ path = '%s/%s/diff' % (self.manager.path, self.get_id())
+ return self.manager.gitlab.http_get(path, **kwargs)
+ @exc.on_http_error(exc.GitlabCherryPickError)
+ def cherry_pick(self, branch, **kwargs):
+ """Cherry-pick a commit into a branch.
-class ProjectFork(GitlabObject):
- _url = '/projects/%(project_id)s/fork'
- canUpdate = False
- canDelete = False
- canList = False
- canGet = False
- requiredUrlAttrs = ['project_id']
- optionalCreateAttrs = ['namespace']
+ Args:
+ branch (str): Name of target branch
+ **kwargs: Extra options to send to the server (e.g. sudo)
+ Raise:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabCherryPickError: If the cherry-pick could not be performed
+ """
+ path = '%s/%s/cherry_pick' % (self.manager.path, self.get_id())
+ post_data = {'branch': branch}
+ self.manager.gitlab.http_post(path, post_data=post_data, **kwargs)
-class ProjectForkManager(BaseManager):
- obj_cls = ProjectFork
+class ProjectCommitManager(RetrieveMixin, CreateMixin, RESTManager):
+ _path = '/projects/%(project_id)s/repository/commits'
+ _obj_cls = ProjectCommit
+ _from_parent_attrs = {'project_id': 'id'}
+ _create_attrs = (('branch', 'commit_message', 'actions'),
+ ('author_email', 'author_name'))
-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', 'job_events', 'wiki_page_events']
- shortPrintAttr = 'url'
+class ProjectEnvironment(SaveMixin, ObjectDeleteMixin, RESTObject):
+ pass
-class ProjectHookManager(BaseManager):
- obj_cls = ProjectHook
+class ProjectEnvironmentManager(GetFromListMixin, CreateMixin, UpdateMixin,
+ DeleteMixin, RESTManager):
+ _path = '/projects/%(project_id)s/environments'
+ _obj_cls = ProjectEnvironment
+ _from_parent_attrs = {'project_id': 'id'}
+ _create_attrs = (('name', ), ('external_url', ))
+ _update_attrs = (tuple(), ('name', 'external_url'))
-class ProjectIssueNote(GitlabObject):
- _url = '/projects/%(project_id)s/issues/%(issue_iid)s/notes'
- _constructorTypes = {'author': 'User'}
- canDelete = False
- requiredUrlAttrs = ['project_id', 'issue_iid']
- requiredCreateAttrs = ['body']
- optionalCreateAttrs = ['created_at']
+class ProjectKey(ObjectDeleteMixin, RESTObject):
+ pass
-class ProjectIssueNoteManager(BaseManager):
- obj_cls = ProjectIssueNote
+class ProjectKeyManager(NoUpdateMixin, RESTManager):
+ _path = '/projects/%(project_id)s/deploy_keys'
+ _obj_cls = ProjectKey
+ _from_parent_attrs = {'project_id': 'id'}
+ _create_attrs = (('title', 'key'), tuple())
-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'
- idAttr = 'iid'
- managers = (
- ('notes', 'ProjectIssueNoteManager',
- [('project_id', 'project_id'), ('issue_iid', 'iid')]),
- )
+ @exc.on_http_error(exc.GitlabProjectDeployKeyError)
+ def enable(self, key_id, **kwargs):
+ """Enable a deploy key for a project.
- def subscribe(self, **kwargs):
- """Subscribe to an issue.
+ Args:
+ key_id (int): The ID of the key to enable
+ **kwargs: Extra options to send to the server (e.g. sudo)
- Raises:
- GitlabConnectionError: If the server cannot be reached.
- GitlabSubscribeError: If the subscription cannot be done
+ Raise:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabProjectDeployKeyError: If the key could not be enabled
"""
- url = ('/projects/%(project_id)s/issues/%(issue_iid)s/subscribe' %
- {'project_id': self.project_id, 'issue_iid': self.iid})
+ path = '%s/%s/enable' % (self.path, key_id)
+ self.gitlab.http_post(path, **kwargs)
- 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.
+class ProjectEvent(RESTObject):
+ _short_print_attr = 'target_title'
- Raises:
- GitlabConnectionError: If the server cannot be reached.
- GitlabUnsubscribeError: If the unsubscription cannot be done
- """
- url = ('/projects/%(project_id)s/issues/%(issue_iid)s/unsubscribe' %
- {'project_id': self.project_id, 'issue_iid': self.iid})
- r = self.gitlab._raw_post(url, **kwargs)
- raise_error_from_response(r, GitlabUnsubscribeError, [201, 304])
- self._set_from_dict(r.json())
+class ProjectEventManager(GetFromListMixin, RESTManager):
+ _path = '/projects/%(project_id)s/events'
+ _obj_cls = ProjectEvent
+ _from_parent_attrs = {'project_id': 'id'}
- 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_iid)s/move' %
- {'project_id': self.project_id, 'issue_iid': self.iid})
+class ProjectFork(RESTObject):
+ pass
- 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.
+class ProjectForkManager(CreateMixin, RESTManager):
+ _path = '/projects/%(project_id)s/fork'
+ _obj_cls = ProjectFork
+ _from_parent_attrs = {'project_id': 'id'}
+ _create_attrs = (tuple(), ('namespace', ))
- Raises:
- GitlabConnectionError: If the server cannot be reached.
- """
- url = ('/projects/%(project_id)s/issues/%(issue_iid)s/todo' %
- {'project_id': self.project_id, 'issue_iid': self.iid})
- 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.
+class ProjectHook(SaveMixin, ObjectDeleteMixin, RESTObject):
+ _short_print_attr = 'url'
- Raises:
- GitlabConnectionError: If the server cannot be reached.
- """
- url = ('/projects/%(project_id)s/issues/%(issue_iid)s/time_stats' %
- {'project_id': self.project_id, 'issue_iid': self.iid})
- r = self.gitlab._raw_get(url, **kwargs)
- raise_error_from_response(r, GitlabGetError)
- return r.json()
- def time_estimate(self, duration, **kwargs):
- """Set an estimated time of work for the issue.
+class ProjectHookManager(CRUDMixin, RESTManager):
+ _path = '/projects/%(project_id)s/hooks'
+ _obj_cls = ProjectHook
+ _from_parent_attrs = {'project_id': 'id'}
+ _create_attrs = (
+ ('url', ),
+ ('push_events', 'issues_events', 'note_events',
+ 'merge_requests_events', 'tag_push_events', 'build_events',
+ 'enable_ssl_verification', 'token', 'pipeline_events')
+ )
+ _update_attrs = (
+ ('url', ),
+ ('push_events', 'issues_events', 'note_events',
+ 'merge_requests_events', 'tag_push_events', 'build_events',
+ 'enable_ssl_verification', 'token', 'pipeline_events')
+ )
- Args:
- duration (str): duration in human format (e.g. 3h30)
- Raises:
- GitlabConnectionError: If the server cannot be reached.
- """
- url = ('/projects/%(project_id)s/issues/%(issue_iid)s/time_estimate' %
- {'project_id': self.project_id, 'issue_iid': self.iid})
- data = {'duration': duration}
- r = self.gitlab._raw_post(url, data, **kwargs)
- raise_error_from_response(r, GitlabTimeTrackingError, 200)
- return r.json()
+class ProjectIssueNote(SaveMixin, ObjectDeleteMixin, RESTObject):
+ _constructor_types = {'author': 'User'}
- 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_iid)s/'
- 'reset_time_estimate' %
- {'project_id': self.project_id, 'issue_iid': self.iid})
- r = self.gitlab._raw_post(url, **kwargs)
- raise_error_from_response(r, GitlabTimeTrackingError, 200)
- return r.json()
+class ProjectIssueNoteManager(CRUDMixin, RESTManager):
+ _path = '/projects/%(project_id)s/issues/%(issue_iid)s/notes'
+ _obj_cls = ProjectIssueNote
+ _from_parent_attrs = {'project_id': 'project_id', 'issue_iid': 'iid'}
+ _create_attrs = (('body', ), ('created_at'))
+ _update_attrs = (('body', ), tuple())
- def add_spent_time(self, duration, **kwargs):
- """Set an estimated time of work for the issue.
- Args:
- duration (str): duration in human format (e.g. 3h30)
+class ProjectIssue(SubscribableMixin, TodoMixin, TimeTrackingMixin, SaveMixin,
+ ObjectDeleteMixin, RESTObject):
+ _constructor_types = {'author': 'User', 'assignee': 'User', 'milestone':
+ 'ProjectMilestone'}
+ _short_print_attr = 'title'
+ _id_attr = 'iid'
+ _managers = (('notes', 'ProjectIssueNoteManager'), )
- Raises:
- GitlabConnectionError: If the server cannot be reached.
- """
- url = ('/projects/%(project_id)s/issues/%(issue_iid)s/'
- 'add_spent_time' %
- {'project_id': self.project_id, 'issue_iid': self.iid})
- data = {'duration': duration}
- r = self.gitlab._raw_post(url, data, **kwargs)
- raise_error_from_response(r, GitlabTimeTrackingError, 201)
- return r.json()
+ @exc.on_http_error(exc.GitlabUpdateError)
+ def move(self, to_project_id, **kwargs):
+ """Move the issue to another project.
- def reset_spent_time(self, **kwargs):
- """Set an estimated time of work for the issue.
+ Args:
+ to_project_id(int): ID of the target project
+ **kwargs: Extra options to send to the server (e.g. sudo)
Raises:
- GitlabConnectionError: If the server cannot be reached.
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabUpdateError: If the issue could not be moved
"""
- url = ('/projects/%(project_id)s/issues/%(issue_iid)s/'
- 'reset_spent_time' %
- {'project_id': self.project_id, 'issue_iid': self.iid})
- r = self.gitlab._raw_post(url, **kwargs)
- raise_error_from_response(r, GitlabTimeTrackingError, 200)
- return r.json()
+ path = '%s/%s/move' % (self.manager.path, self.get_id())
+ data = {'to_project_id': to_project_id}
+ server_data = self.manager.gitlab.http_post(path, post_data=data,
+ **kwargs)
+ self._update_attrs(server_data)
-class ProjectIssueManager(BaseManager):
- obj_cls = ProjectIssue
+class ProjectIssueManager(CRUDMixin, RESTManager):
+ _path = '/projects/%(project_id)s/issues/'
+ _obj_cls = ProjectIssue
+ _from_parent_attrs = {'project_id': 'id'}
+ _list_filters = ('state', 'labels', 'milestone', 'order_by', 'sort')
+ _create_attrs = (('title', ),
+ ('description', 'assignee_id', 'milestone_id', 'labels',
+ 'created_at', 'due_date'))
+ _update_attrs = (tuple(), ('title', 'description', 'assignee_id',
+ 'milestone_id', 'labels', 'created_at',
+ 'updated_at', 'state_event', 'due_date'))
-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 ProjectMember(SaveMixin, ObjectDeleteMixin, RESTObject):
+ _short_print_attr = 'username'
-class ProjectMemberManager(BaseManager):
- obj_cls = ProjectMember
+class ProjectMemberManager(CRUDMixin, RESTManager):
+ _path = '/projects/%(project_id)s/members'
+ _obj_cls = ProjectMember
+ _from_parent_attrs = {'project_id': 'id'}
+ _create_attrs = (('access_level', 'user_id'), ('expires_at', ))
+ _update_attrs = (('access_level', ), ('expires_at', ))
-class ProjectNote(GitlabObject):
- _url = '/projects/%(project_id)s/notes'
- _constructorTypes = {'author': 'User'}
- canUpdate = False
- canDelete = False
- requiredUrlAttrs = ['project_id']
- requiredCreateAttrs = ['body']
+class ProjectNote(RESTObject):
+ _constructor_types = {'author': 'User'}
-class ProjectNoteManager(BaseManager):
- obj_cls = ProjectNote
+class ProjectNoteManager(RetrieveMixin, RESTManager):
+ _path = '/projects/%(project_id)s/notes'
+ _obj_cls = ProjectNote
+ _from_parent_attrs = {'project_id': 'id'}
+ _create_attrs = (('body', ), tuple())
class ProjectNotificationSettings(NotificationSettings):
- _url = '/projects/%(project_id)s/notification_settings'
- requiredUrlAttrs = ['project_id']
+ pass
-class ProjectNotificationSettingsManager(BaseManager):
- obj_cls = ProjectNotificationSettings
+class ProjectNotificationSettingsManager(NotificationSettingsManager):
+ _path = '/projects/%(project_id)s/notification_settings'
+ _obj_cls = ProjectNotificationSettings
+ _from_parent_attrs = {'project_id': 'id'}
-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(ObjectDeleteMixin, RESTObject):
+ _constructor_types = {'release': 'ProjectTagRelease',
+ 'commit': 'ProjectCommit'}
+ _id_attr = 'name'
+ _short_print_attr = 'name'
-
-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):
+ def set_release_description(self, description, **kwargs):
"""Set the release notes on the tag.
If the release doesn't exist yet, it will be created. If it already
@@ -1076,172 +987,151 @@ class ProjectTag(GitlabObject):
Args:
description (str): Description of the release.
+ **kwargs: Extra options to send to the server (e.g. sudo)
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.
+ GitlabAuthenticationError: If authentication is not correct
+ 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)
+ path = '%s/%s/release' % (self.manager.path, self.get_id())
+ data = {'description': description}
if self.release is None:
- r = self.gitlab._raw_post(url, data={'description': description})
- raise_error_from_response(r, GitlabCreateError, 201)
+ try:
+ server_data = self.manager.gitlab.http_post(path,
+ post_data=data,
+ **kwargs)
+ except exc.GitlabHttpError as e:
+ raise exc.GitlabCreateError(e.response_code, e.error_message)
else:
- r = self.gitlab._raw_put(url, data={'description': description})
- raise_error_from_response(r, GitlabUpdateError, 200)
- self.release = ProjectTagRelease(self, r.json())
+ try:
+ server_data = self.manager.gitlab.http_put(path,
+ post_data=data,
+ **kwargs)
+ except exc.GitlabHttpError as e:
+ raise exc.GitlabUpdateError(e.response_code, e.error_message)
+ self.release = server_data
-class ProjectTagManager(BaseManager):
- obj_cls = ProjectTag
+class ProjectTagManager(GetFromListMixin, CreateMixin, DeleteMixin,
+ RESTManager):
+ _path = '/projects/%(project_id)s/repository/tags'
+ _obj_cls = ProjectTag
+ _from_parent_attrs = {'project_id': 'id'}
+ _create_attrs = (('tag_name', 'ref'), ('message',))
-class ProjectMergeRequestDiff(GitlabObject):
- _url = ('/projects/%(project_id)s/merge_requests/'
- '%(merge_request_iid)s/versions')
- canCreate = False
- canUpdate = False
- canDelete = False
- requiredUrlAttrs = ['project_id', 'merge_request_iid']
+class ProjectMergeRequestDiff(RESTObject):
+ pass
-class ProjectMergeRequestDiffManager(BaseManager):
- obj_cls = ProjectMergeRequestDiff
+class ProjectMergeRequestDiffManager(RetrieveMixin, RESTManager):
+ _path = '/projects/%(project_id)s/merge_requests/%(mr_iid)s/versions'
+ _obj_cls = ProjectMergeRequestDiff
+ _from_parent_attrs = {'project_id': 'project_id', 'mr_iid': 'iid'}
-class ProjectMergeRequestNote(GitlabObject):
- _url = ('/projects/%(project_id)s/merge_requests/%(merge_request_iid)s'
- '/notes')
- _constructorTypes = {'author': 'User'}
- requiredUrlAttrs = ['project_id', 'merge_request_iid']
- requiredCreateAttrs = ['body']
+class ProjectMergeRequestNote(SaveMixin, ObjectDeleteMixin, RESTObject):
+ _constructor_types = {'author': 'User'}
-class ProjectMergeRequestNoteManager(BaseManager):
- obj_cls = ProjectMergeRequestNote
+class ProjectMergeRequestNoteManager(CRUDMixin, RESTManager):
+ _path = '/projects/%(project_id)s/merge_requests/%(mr_iid)s/notes'
+ _obj_cls = ProjectMergeRequestNote
+ _from_parent_attrs = {'project_id': 'project_id', 'mr_iid': 'iid'}
+ _create_attrs = (('body', ), tuple())
+ _update_attrs = (('body', ), tuple())
-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']
- idAttr = 'iid'
+class ProjectMergeRequest(SubscribableMixin, TodoMixin, TimeTrackingMixin,
+ SaveMixin, ObjectDeleteMixin, RESTObject):
+ _constructor_types = {'author': 'User', 'assignee': 'User'}
+ _id_attr = 'iid'
- managers = (
- ('notes', 'ProjectMergeRequestNoteManager',
- [('project_id', 'project_id'), ('merge_request_iid', 'iid')]),
- ('diffs', 'ProjectMergeRequestDiffManager',
- [('project_id', 'project_id'), ('merge_request_iid', 'iid')]),
+ _managers = (
+ ('notes', 'ProjectMergeRequestNoteManager'),
+ ('diffs', 'ProjectMergeRequestDiffManager')
)
- 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_iid)s/'
- 'subscribe' %
- {'project_id': self.project_id, 'mr_iid': self.iid})
-
- 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())
+ @exc.on_http_error(exc.GitlabMROnBuildSuccessError)
+ def cancel_merge_when_pipeline_succeeds(self, **kwargs):
+ """Cancel merge when the pipeline succeeds.
- def unsubscribe(self, **kwargs):
- """Unsubscribe a MR.
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
Raises:
- GitlabConnectionError: If the server cannot be reached.
- GitlabUnsubscribeError: If the unsubscription cannot be done
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabMROnBuildSuccessError: If the server could not handle the
+ request
"""
- url = ('/projects/%(project_id)s/merge_requests/%(mr_iid)s/'
- 'unsubscribe' %
- {'project_id': self.project_id, 'mr_iid': self.iid})
- 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.iid))
- 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())
+ path = ('%s/%s/cancel_merge_when_pipeline_succeeds' %
+ (self.manager.path, self.get_id()))
+ server_data = self.manager.gitlab.http_put(path, **kwargs)
+ self._update_attrs(server_data)
+ @exc.on_http_error(exc.GitlabListError)
def closes_issues(self, **kwargs):
- """List issues closed by the MR.
+ """List issues that will close on merge."
- Returns:
- list (ProjectIssue): List of closed issues
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
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.iid))
- return self.gitlab._raw_list(url, ProjectIssue, **kwargs)
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabListError: If the list could not be retrieved
+ Returns:
+ RESTObjectList: List of issues
+ """
+ path = '%s/%s/closes_issues' % (self.manager.path, self.get_id())
+ data_list = self.manager.gitlab.http_list(path, as_list=False,
+ **kwargs)
+ manager = ProjectIssueManager(self.manager.gitlab,
+ parent=self.manager._parent)
+ return RESTObjectList(manager, ProjectIssue, data_list)
+
+ @exc.on_http_error(exc.GitlabListError)
def commits(self, **kwargs):
"""List the merge request commits.
- Returns:
- list (ProjectCommit): List of commits
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
Raises:
- GitlabConnectionError: If the server cannot be reached.
- GitlabListError: If the server fails to perform the request.
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabListError: If the list could not be retrieved
+
+ Returns:
+ RESTObjectList: The list of commits
"""
- url = ('/projects/%s/merge_requests/%s/commits' %
- (self.project_id, self.iid))
- return self.gitlab._raw_list(url, ProjectCommit, **kwargs)
+ path = '%s/%s/commits' % (self.manager.path, self.get_id())
+ data_list = self.manager.gitlab.http_list(path, as_list=False,
+ **kwargs)
+ manager = ProjectCommitManager(self.manager.gitlab,
+ parent=self.manager._parent)
+ return RESTObjectList(manager, ProjectCommit, data_list)
+
+ @exc.on_http_error(exc.GitlabListError)
def changes(self, **kwargs):
"""List the merge request changes.
- Returns:
- list (dict): List of changes
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
Raises:
- GitlabConnectionError: If the server cannot be reached.
- GitlabListError: If the server fails to perform the request.
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabListError: If the list could not be retrieved
+
+ Returns:
+ RESTObjectList: List of changes
"""
- url = ('/projects/%s/merge_requests/%s/changes' %
- (self.project_id, self.iid))
- r = self.gitlab._raw_get(url, **kwargs)
- raise_error_from_response(r, GitlabListError)
- return r.json()
+ path = '%s/%s/changes' % (self.manager.path, self.get_id())
+ return self.manager.gitlab.http_get(path, **kwargs)
+ @exc.on_http_error(exc.GitlabMRClosedError)
def merge(self, merge_commit_message=None,
should_remove_source_branch=False,
merge_when_pipeline_succeeds=False,
@@ -1254,17 +1144,13 @@ class ProjectMergeRequest(GitlabObject):
branch
merge_when_pipeline_succeeds (bool): Wait for the build to succeed,
then merge
+ **kwargs: Extra options to send to the server (e.g. sudo)
- 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
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabMRClosedError: If the merge failed
"""
- url = '/projects/%s/merge_requests/%s/merge' % (self.project_id,
- self.iid)
+ path = '%s/%s/merge' % (self.manager.path, self.get_id())
data = {}
if merge_commit_message:
data['merge_commit_message'] = merge_commit_message
@@ -1273,205 +1159,253 @@ class ProjectMergeRequest(GitlabObject):
if merge_when_pipeline_succeeds:
data['merge_when_pipeline_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())
+ server_data = self.manager.gitlab.http_put(path, post_data=data,
+ **kwargs)
+ self._update_attrs(server_data)
- 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_iid)s/todo' %
- {'project_id': self.project_id, 'mr_iid': self.iid})
- r = self.gitlab._raw_post(url, **kwargs)
- raise_error_from_response(r, GitlabTodoError, [201, 304])
+class ProjectMergeRequestManager(CRUDMixin, RESTManager):
+ _path = '/projects/%(project_id)s/merge_requests'
+ _obj_cls = ProjectMergeRequest
+ _from_parent_attrs = {'project_id': 'id'}
+ _create_attrs = (
+ ('source_branch', 'target_branch', 'title'),
+ ('assignee_id', 'description', 'target_project_id', 'labels',
+ 'milestone_id', 'remove_source_branch')
+ )
+ _update_attrs = (tuple(), ('target_branch', 'assignee_id', 'title',
+ 'description', 'state_event', 'labels',
+ 'milestone_id'))
+ _list_filters = ('iids', 'state', 'order_by', 'sort')
- 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_iid)s/'
- 'time_stats' %
- {'project_id': self.project_id, 'mr_iid': self.iid})
- r = self.gitlab._raw_get(url, **kwargs)
- raise_error_from_response(r, GitlabGetError)
- return r.json()
+class ProjectMilestone(SaveMixin, ObjectDeleteMixin, RESTObject):
+ _short_print_attr = 'title'
- def time_estimate(self, duration, **kwargs):
- """Set an estimated time of work for the merge request.
+ @exc.on_http_error(exc.GitlabListError)
+ def issues(self, **kwargs):
+ """List issues related to this milestone.
Args:
- duration (str): duration in human format (e.g. 3h30)
+ **kwargs: Extra options to send to the server (e.g. sudo)
Raises:
- GitlabConnectionError: If the server cannot be reached.
- """
- url = ('/projects/%(project_id)s/merge_requests/%(mr_iid)s/'
- 'time_estimate' %
- {'project_id': self.project_id, 'mr_iid': self.iid})
- data = {'duration': duration}
- r = self.gitlab._raw_post(url, data, **kwargs)
- raise_error_from_response(r, GitlabTimeTrackingError, 200)
- return r.json()
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabListError: If the list could not be retrieved
- def reset_time_estimate(self, **kwargs):
- """Resets estimated time for the merge request to 0 seconds.
-
- Raises:
- GitlabConnectionError: If the server cannot be reached.
+ Returns:
+ RESTObjectList: The list of issues
"""
- url = ('/projects/%(project_id)s/merge_requests/%(mr_iid)s/'
- 'reset_time_estimate' %
- {'project_id': self.project_id, 'mr_iid': self.iid})
- r = self.gitlab._raw_post(url, **kwargs)
- raise_error_from_response(r, GitlabTimeTrackingError, 200)
- return r.json()
- def add_spent_time(self, duration, **kwargs):
- """Set an estimated time of work for the merge request.
+ path = '%s/%s/issues' % (self.manager.path, self.get_id())
+ data_list = self.manager.gitlab.http_list(path, as_list=False,
+ **kwargs)
+ manager = ProjectCommitManager(self.manager.gitlab,
+ parent=self.manager._parent)
+ # FIXME(gpocentek): the computed manager path is not correct
+ return RESTObjectList(manager, ProjectIssue, data_list)
+
+ @exc.on_http_error(exc.GitlabListError)
+ def merge_requests(self, **kwargs):
+ """List the merge requests related to this milestone.
Args:
- duration (str): duration in human format (e.g. 3h30)
+ **kwargs: Extra options to send to the server (e.g. sudo)
Raises:
- GitlabConnectionError: If the server cannot be reached.
- """
- url = ('/projects/%(project_id)s/merge_requests/%(mr_iid)s/'
- 'add_spent_time' %
- {'project_id': self.project_id, 'mr_iid': self.iid})
- data = {'duration': duration}
- r = self.gitlab._raw_post(url, data, **kwargs)
- raise_error_from_response(r, GitlabTimeTrackingError, 201)
- return r.json()
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabListError: If the list could not be retrieved
- def reset_spent_time(self, **kwargs):
- """Set an estimated time of work for the merge request.
-
- Raises:
- GitlabConnectionError: If the server cannot be reached.
+ Returns:
+ RESTObjectList: The list of merge requests
"""
- url = ('/projects/%(project_id)s/merge_requests/%(mr_iid)s/'
- 'reset_spent_time' %
- {'project_id': self.project_id, 'mr_iid': self.iid})
- r = self.gitlab._raw_post(url, **kwargs)
- raise_error_from_response(r, GitlabTimeTrackingError, 200)
- return r.json()
+ path = '%s/%s/merge_requests' % (self.manager.path, self.get_id())
+ data_list = self.manager.gitlab.http_list(path, as_list=False,
+ **kwargs)
+ manager = ProjectCommitManager(self.manager.gitlab,
+ parent=self.manager._parent)
+ # FIXME(gpocentek): the computed manager path is not correct
+ return RESTObjectList(manager, ProjectMergeRequest, data_list)
-class ProjectMergeRequestManager(BaseManager):
- obj_cls = ProjectMergeRequest
+class ProjectMilestoneManager(CRUDMixin, RESTManager):
+ _path = '/projects/%(project_id)s/milestones'
+ _obj_cls = ProjectMilestone
+ _from_parent_attrs = {'project_id': 'id'}
+ _create_attrs = (('title', ), ('description', 'due_date', 'start_date',
+ 'state_event'))
+ _update_attrs = (tuple(), ('title', 'description', 'due_date',
+ 'start_date', 'state_event'))
+ _list_filters = ('iids', 'state')
-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'
+class ProjectLabel(SubscribableMixin, SaveMixin, ObjectDeleteMixin,
+ RESTObject):
+ _id_attr = 'name'
- def issues(self, **kwargs):
- url = "/projects/%s/milestones/%s/issues" % (self.project_id, self.id)
- return self.gitlab._raw_list(url, ProjectIssue, **kwargs)
+ # Update without ID, but we need an ID to get from list.
+ @exc.on_http_error(exc.GitlabUpdateError)
+ def save(self, **kwargs):
+ """Saves the changes made to the object to the server.
- def merge_requests(self, **kwargs):
- """List the merge requests related to this milestone
+ The object is updated to match what the server returns.
- Returns:
- list (ProjectMergeRequest): List of merge requests
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
Raises:
- GitlabConnectionError: If the server cannot be reached.
- GitlabListError: If the server fails to perform the request.
+ GitlabAuthenticationError: If authentication is not correct.
+ GitlabUpdateError: If the server cannot perform the request.
"""
- url = ('/projects/%s/milestones/%s/merge_requests' %
- (self.project_id, self.id))
- return self.gitlab._raw_list(url, ProjectMergeRequest, **kwargs)
+ updated_data = self._get_updated_data()
+ # call the manager
+ server_data = self.manager.update(None, updated_data, **kwargs)
+ self._update_attrs(server_data)
-class ProjectMilestoneManager(BaseManager):
- obj_cls = ProjectMilestone
+class ProjectLabelManager(GetFromListMixin, CreateMixin, UpdateMixin,
+ DeleteMixin, RESTManager):
+ _path = '/projects/%(project_id)s/labels'
+ _obj_cls = ProjectLabel
+ _from_parent_attrs = {'project_id': 'id'}
+ _create_attrs = (('name', 'color'), ('description', 'priority'))
+ _update_attrs = (('name', ),
+ ('new_name', 'color', 'description', 'priority'))
-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']
+ # Delete without ID.
+ @exc.on_http_error(exc.GitlabDeleteError)
+ def delete(self, name, **kwargs):
+ """Delete a Label on the server.
- def subscribe(self, **kwargs):
- """Subscribe to a label.
+ Args:
+ name: The name of the label
+ **kwargs: Extra options to send to the Gitlab server (e.g. sudo)
Raises:
- GitlabConnectionError: If the server cannot be reached.
- GitlabSubscribeError: If the subscription cannot be done
+ GitlabAuthenticationError: If authentication is not correct.
+ GitlabDeleteError: If the server cannot perform the request.
+ """
+ self.gitlab.http_delete(self.path, query_data={'name': name}, **kwargs)
+
+
+class ProjectFile(SaveMixin, ObjectDeleteMixin, RESTObject):
+ _id_attr = 'file_path'
+ _short_print_attr = 'file_path'
+
+ def decode(self):
+ """Returns the decoded content of the file.
+
+ Returns:
+ (str): the decoded content.
+ """
+ return base64.b64decode(self.content)
+
+ def save(self, branch, commit_message, **kwargs):
+ """Save the changes made to the file to the server.
+
+ The object is updated to match what the server returns.
+
+ Args:
+ branch (str): Branch in which the file will be updated
+ commit_message (str): Message to send with the commit
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ Raise:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabUpdateError: If the server cannot perform the request
"""
- url = ('/projects/%(project_id)s/labels/%(label_id)s/subscribe' %
- {'project_id': self.project_id, 'label_id': self.name})
+ self.branch = branch
+ self.commit_message = commit_message
+ super(ProjectFile, self).save(**kwargs)
- r = self.gitlab._raw_post(url, **kwargs)
- raise_error_from_response(r, GitlabSubscribeError, [201, 304])
- self._set_from_dict(r.json())
+ def delete(self, branch, commit_message, **kwargs):
+ """Delete the file from the server.
- def unsubscribe(self, **kwargs):
- """Unsubscribe a label.
+ Args:
+ branch (str): Branch from which the file will be removed
+ commit_message (str): Commit message for the deletion
+ **kwargs: Extra options to send to the server (e.g. sudo)
Raises:
- GitlabConnectionError: If the server cannot be reached.
- GitlabUnsubscribeError: If the unsubscription cannot be done
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabDeleteError: If the server cannot perform the request
"""
- url = ('/projects/%(project_id)s/labels/%(label_id)s/unsubscribe' %
- {'project_id': self.project_id, 'label_id': self.name})
+ self.manager.delete(self.get_id(), branch, commit_message, **kwargs)
+
+
+class ProjectFileManager(GetMixin, CreateMixin, UpdateMixin, DeleteMixin,
+ RESTManager):
+ _path = '/projects/%(project_id)s/repository/files'
+ _obj_cls = ProjectFile
+ _from_parent_attrs = {'project_id': 'id'}
+ _create_attrs = (('file_path', 'branch', 'content', 'commit_message'),
+ ('encoding', 'author_email', 'author_name'))
+ _update_attrs = (('file_path', 'branch', 'content', 'commit_message'),
+ ('encoding', 'author_email', 'author_name'))
- r = self.gitlab._raw_post(url, **kwargs)
- raise_error_from_response(r, GitlabUnsubscribeError, [201, 304])
- self._set_from_dict(r.json())
+ def get(self, file_path, ref, **kwargs):
+ """Retrieve a single file.
+ Args:
+ file_path (str): Path of the file to retrieve
+ ref (str): Name of the branch, tag or commit
+ **kwargs: Extra options to send to the Gitlab server (e.g. sudo)
-class ProjectLabelManager(BaseManager):
- obj_cls = ProjectLabel
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabGetError: If the file could not be retrieved
+ Returns:
+ object: The generated RESTObject
+ """
+ file_path = file_path.replace('/', '%2F')
+ return GetMixin.get(self, file_path, ref=ref, **kwargs)
-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'
+ @exc.on_http_error(exc.GitlabCreateError)
+ def create(self, data, **kwargs):
+ """Create a new object.
- def decode(self):
- """Returns the decoded content of the file.
+ Args:
+ data (dict): parameters to send to the server to create the
+ resource
+ **kwargs: Extra options to send to the Gitlab server (e.g. sudo)
Returns:
- (str): the decoded content.
+ RESTObject: a new instance of the managed object class built with
+ the data sent by the server
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabCreateError: If the server cannot perform the request
"""
- return base64.b64decode(self.content)
+ self._check_missing_create_attrs(data)
+ file_path = data.pop('file_path')
+ path = '%s/%s' % (self.path, file_path)
+ server_data = self.gitlab.http_post(path, post_data=data, **kwargs)
+ return self._obj_cls(self, server_data)
+
+ @exc.on_http_error(exc.GitlabDeleteError)
+ def delete(self, file_path, branch, commit_message, **kwargs):
+ """Delete a file on the server.
-class ProjectFileManager(BaseManager):
- obj_cls = ProjectFile
+ Args:
+ file_path (str): Path of the file to remove
+ branch (str): Branch from which the file will be removed
+ commit_message (str): Commit message for the deletion
+ **kwargs: Extra options to send to the Gitlab server (e.g. sudo)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabDeleteError: If the server cannot perform the request
+ """
+ path = '%s/%s' % (self.path, file_path.replace('/', '%2F'))
+ data = {'branch': branch, 'commit_message': commit_message}
+ self.gitlab.http_delete(path, query_data=data, **kwargs)
- def raw(self, filepath, ref, streamed=False, action=None, chunk_size=1024,
+ @exc.on_http_error(exc.GitlabGetError)
+ def raw(self, file_path, ref, streamed=False, action=None, chunk_size=1024,
**kwargs):
"""Return the content of a file for a commit.
@@ -1480,165 +1414,172 @@ class ProjectFileManager(BaseManager):
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.
+ treatment
action (callable): Callable responsible of dealing with chunk of
- data.
- chunk_size (int): Size of each chunk.
+ data
+ chunk_size (int): Size of each chunk
+ **kwargs: Extra options to send to the Gitlab server (e.g. sudo)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabGetError: If the file could not be retrieved
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)
+ file_path = file_path.replace('/', '%2F').replace('.', '%2E')
+ path = '%s/%s/raw' % (self.path, file_path)
+ query_data = {'ref': ref}
+ result = self.gitlab.http_get(path, query_data=query_data,
+ streamed=streamed, **kwargs)
+ return utils.response_content(result, streamed, action, chunk_size)
-class ProjectPipeline(GitlabObject):
- _url = '/projects/%(project_id)s/pipelines'
- _create_url = '/projects/%(project_id)s/pipeline'
+class ProjectPipeline(RESTObject):
+ @exc.on_http_error(exc.GitlabPipelineCancelError)
+ def cancel(self, **kwargs):
+ """Cancel the job.
- canUpdate = False
- canDelete = False
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
- requiredUrlAttrs = ['project_id']
- requiredCreateAttrs = ['ref']
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabPipelineCancelError: If the request failed
+ """
+ path = '%s/%s/cancel' % (self.manager.path, self.get_id())
+ self.manager.gitlab.http_post(path)
+ @exc.on_http_error(exc.GitlabPipelineRetryError)
def retry(self, **kwargs):
- """Retries failed builds in a pipeline.
+ """Retry the job.
+
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
Raises:
- GitlabConnectionError: If the server cannot be reached.
- GitlabPipelineRetryError: If the retry cannot be done.
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabPipelineRetryError: If the request failed
"""
- 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())
+ path = '%s/%s/retry' % (self.manager.path, self.get_id())
+ self.manager.gitlab.http_post(path)
- 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(RetrieveMixin, CreateMixin, RESTManager):
+ _path = '/projects/%(project_id)s/pipelines'
+ _obj_cls = ProjectPipeline
+ _from_parent_attrs = {'project_id': 'id'}
+ _create_attrs = (('ref', ), tuple())
+ def create(self, data, **kwargs):
+ """Creates a new object.
-class ProjectPipelineManager(BaseManager):
- obj_cls = ProjectPipeline
+ Args:
+ data (dict): Parameters to send to the server to create the
+ resource
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabCreateError: If the server cannot perform the request
+
+ Returns:
+ RESTObject: A new instance of the managed object class build with
+ the data sent by the server
+ """
+ path = self.path[:-1] # drop the 's'
+ return CreateMixin.create(self, data, path=path, **kwargs)
-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 ProjectSnippetNote(RESTObject):
+ _constructor_types = {'author': 'User'}
-class ProjectSnippetNoteManager(BaseManager):
- obj_cls = ProjectSnippetNote
+class ProjectSnippetNoteManager(RetrieveMixin, CreateMixin, RESTManager):
+ _path = '/projects/%(project_id)s/snippets/%(snippet_id)s/notes'
+ _obj_cls = ProjectSnippetNote
+ _from_parent_attrs = {'project_id': 'project_id',
+ 'snippet_id': 'id'}
+ _create_attrs = (('body', ), tuple())
-class ProjectSnippet(GitlabObject):
+class ProjectSnippet(SaveMixin, ObjectDeleteMixin, RESTObject):
_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')]),
- )
+ _constructor_types = {'author': 'User'}
+ _short_print_attr = 'title'
+ _managers = (('notes', 'ProjectSnippetNoteManager'), )
+ @exc.on_http_error(exc.GitlabGetError)
def content(self, streamed=False, action=None, chunk_size=1024, **kwargs):
- """Return the raw content of a snippet.
+ """Return the 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.
+ data
+ chunk_size (int): Size of each chunk
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabGetError: If the content could not be retrieved
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)
-
+ path = "%s/%s/raw" % (self.manager.path, self.get_id())
+ result = self.manager.gitlab.http_get(path, streamed=streamed,
+ **kwargs)
+ return utils.response_content(result, streamed, action, chunk_size)
-class ProjectSnippetManager(BaseManager):
- obj_cls = ProjectSnippet
+class ProjectSnippetManager(CRUDMixin, RESTManager):
+ _path = '/projects/%(project_id)s/snippets'
+ _obj_cls = ProjectSnippet
+ _from_parent_attrs = {'project_id': 'id'}
+ _create_attrs = (('title', 'file_name', 'code'),
+ ('lifetime', 'visibility'))
+ _update_attrs = (tuple(), ('title', 'file_name', 'code', 'visibility'))
-class ProjectTrigger(GitlabObject):
- _url = '/projects/%(project_id)s/triggers'
- requiredUrlAttrs = ['project_id']
- requiredCreateAttrs = ['description']
- optionalUpdateAttrs = ['description']
+class ProjectTrigger(SaveMixin, ObjectDeleteMixin, RESTObject):
def take_ownership(self, **kwargs):
- """Update the owner of a trigger.
+ """Update the owner of a trigger."""
+ path = '%s/%s/take_ownership' % (self.manager.path, self.get_id())
+ server_data = self.manager.gitlab.http_post(path, **kwargs)
+ self._update_attrs(server_data)
- Raises:
- GitlabConnectionError: If the server cannot be reached.
- GitlabGetError: If the server fails to perform the request.
- """
- url = ('/projects/%(project_id)s/triggers/%(id)s/take_ownership' %
- {'project_id': self.project_id, 'id': self.id})
- r = self.gitlab._raw_post(url, **kwargs)
- raise_error_from_response(r, GitlabUpdateError, 200)
- self._set_from_dict(r.json())
+
+class ProjectTriggerManager(CRUDMixin, RESTManager):
+ _path = '/projects/%(project_id)s/triggers'
+ _obj_cls = ProjectTrigger
+ _from_parent_attrs = {'project_id': 'id'}
+ _create_attrs = (('description', ), tuple())
+ _update_attrs = (('description', ), tuple())
-class ProjectTriggerManager(BaseManager):
- obj_cls = ProjectTrigger
+class ProjectVariable(SaveMixin, ObjectDeleteMixin, RESTObject):
+ _id_attr = 'key'
-class ProjectVariable(GitlabObject):
- _url = '/projects/%(project_id)s/variables'
- idAttr = 'key'
- requiredUrlAttrs = ['project_id']
- requiredCreateAttrs = ['key', 'value']
+class ProjectVariableManager(CRUDMixin, RESTManager):
+ _path = '/projects/%(project_id)s/variables'
+ _obj_cls = ProjectVariable
+ _from_parent_attrs = {'project_id': 'id'}
+ _create_attrs = (('key', 'value'), tuple())
+ _update_attrs = (('key', 'value'), tuple())
-class ProjectVariableManager(BaseManager):
- obj_cls = ProjectVariable
+class ProjectService(SaveMixin, ObjectDeleteMixin, RESTObject):
+ pass
-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']
+class ProjectServiceManager(GetMixin, UpdateMixin, DeleteMixin, RESTManager):
+ _path = '/projects/%(project_id)s/services'
+ _from_parent_attrs = {'project_id': 'id'}
+ _obj_cls = ProjectService
_service_attrs = {
'asana': (('api_key', ), ('restrict_to_branch', )),
@@ -1664,16 +1605,10 @@ class ProjectService(GitlabObject):
'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')),
+ 'jira': (('url', 'project_key'),
+ ('new_issue_url', 'project_url', 'issues_url', 'api_url',
+ 'description', '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'),
@@ -1683,33 +1618,44 @@ class ProjectService(GitlabObject):
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)
+ def get(self, id, **kwargs):
+ """Retrieve a single object.
+
+ Args:
+ id (int or str): ID of the object to retrieve
+ lazy (bool): If True, don't request the server, but create a
+ shallow object giving access to the managers. This is
+ useful if you want to avoid useless calls to the API.
+ **kwargs: Extra options to send to the Gitlab server (e.g. sudo)
+
+ Returns:
+ object: The generated RESTObject.
- if missing:
- raise GitlabUpdateError('Missing attribute(s): %s' %
- ", ".join(missing))
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabGetError: If the server cannot perform the request
+ """
+ obj = super(ProjectServiceManager, self).get(id, **kwargs)
+ obj.id = id
+ return obj
- # Optional args
- for attr in self._service_attrs[self.service_name][1]:
- if hasattr(self, attr):
- data[attr] = getattr(self, attr)
+ def update(self, id=None, new_data={}, **kwargs):
+ """Update an object on the server.
- return json.dumps(data)
+ Args:
+ id: ID of the object to update (can be None if not required)
+ new_data: the update data for the object
+ **kwargs: Extra options to send to the Gitlab server (e.g. sudo)
+ Returns:
+ dict: The new object data (*not* a RESTObject)
-class ProjectServiceManager(BaseManager):
- obj_cls = ProjectService
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabUpdateError: If the server cannot perform the request
+ """
+ super(ProjectServiceManager, self).update(id, new_data, **kwargs)
+ self.id = id
def available(self, **kwargs):
"""List the services known by python-gitlab.
@@ -1717,332 +1663,321 @@ class ProjectServiceManager(BaseManager):
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')]),
- ('jobs', 'ProjectJobManager', [('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')]),
+ return list(self._service_attrs.keys())
+
+
+class ProjectAccessRequest(AccessRequestMixin, ObjectDeleteMixin, RESTObject):
+ pass
+
+
+class ProjectAccessRequestManager(GetFromListMixin, CreateMixin, DeleteMixin,
+ RESTManager):
+ _path = '/projects/%(project_id)s/access_requests'
+ _obj_cls = ProjectAccessRequest
+ _from_parent_attrs = {'project_id': 'id'}
+
+
+class ProjectDeployment(RESTObject):
+ pass
+
+
+class ProjectDeploymentManager(RetrieveMixin, RESTManager):
+ _path = '/projects/%(project_id)s/deployments'
+ _obj_cls = ProjectDeployment
+ _from_parent_attrs = {'project_id': 'id'}
+
+
+class ProjectRunner(ObjectDeleteMixin, RESTObject):
+ pass
+
+
+class ProjectRunnerManager(NoUpdateMixin, RESTManager):
+ _path = '/projects/%(project_id)s/runners'
+ _obj_cls = ProjectRunner
+ _from_parent_attrs = {'project_id': 'id'}
+ _create_attrs = (('runner_id', ), tuple())
+
+
+class Project(SaveMixin, ObjectDeleteMixin, RESTObject):
+ _constructor_types = {'owner': 'User', 'namespace': 'Group'}
+ _short_print_attr = 'path'
+ _managers = (
+ ('accessrequests', 'ProjectAccessRequestManager'),
+ ('boards', 'ProjectBoardManager'),
+ ('branches', 'ProjectBranchManager'),
+ ('jobs', 'ProjectJobManager'),
+ ('commits', 'ProjectCommitManager'),
+ ('deployments', 'ProjectDeploymentManager'),
+ ('environments', 'ProjectEnvironmentManager'),
+ ('events', 'ProjectEventManager'),
+ ('files', 'ProjectFileManager'),
+ ('forks', 'ProjectForkManager'),
+ ('hooks', 'ProjectHookManager'),
+ ('keys', 'ProjectKeyManager'),
+ ('issues', 'ProjectIssueManager'),
+ ('labels', 'ProjectLabelManager'),
+ ('members', 'ProjectMemberManager'),
+ ('mergerequests', 'ProjectMergeRequestManager'),
+ ('milestones', 'ProjectMilestoneManager'),
+ ('notes', 'ProjectNoteManager'),
+ ('notificationsettings', 'ProjectNotificationSettingsManager'),
+ ('pipelines', 'ProjectPipelineManager'),
+ ('runners', 'ProjectRunnerManager'),
+ ('services', 'ProjectServiceManager'),
+ ('snippets', 'ProjectSnippetManager'),
+ ('tags', 'ProjectTagManager'),
+ ('triggers', 'ProjectTriggerManager'),
+ ('variables', 'ProjectVariableManager'),
)
+ @exc.on_http_error(exc.GitlabGetError)
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.
+ **kwargs: Extra options to send to the server (e.g. sudo)
Raises:
- GitlabConnectionError: If the server cannot be reached.
- GitlabGetError: If the server fails to perform the request.
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabGetError: If the server failed to perform the request
+
+ Returns:
+ list: The representation of the tree
"""
- url = "/projects/%s/repository/tree" % (self.id)
- params = []
+ gl_path = '/projects/%s/repository/tree' % self.get_id()
+ query_data = {}
if path:
- params.append(urllib.parse.urlencode({'path': path}))
+ query_data['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()
+ query_data['ref'] = ref
+ return self.manager.gitlab.http_get(gl_path, query_data=query_data,
+ **kwargs)
+
+ @exc.on_http_error(exc.GitlabGetError)
+ def repository_blob(self, sha, **kwargs):
+ """Return a blob by blob SHA.
+
+ Args:
+ sha(str): ID of the blob
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabGetError: If the server failed to perform the request
+
+ Returns:
+ str: The blob metadata
+ """
+
+ path = '/projects/%s/repository/blobs/%s' % (self.get_id(), sha)
+ return self.manager.gitlab.http_get(path, **kwargs)
+ @exc.on_http_error(exc.GitlabGetError)
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.
+ """Return the raw file contents for a blob.
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.
+ treatment
action (callable): Callable responsible of dealing with chunk of
- data.
- chunk_size (int): Size of each chunk.
-
- Returns:
- str: The blob content
+ data
+ chunk_size (int): Size of each chunk
+ **kwargs: Extra options to send to the server (e.g. sudo)
Raises:
- GitlabConnectionError: If the server cannot be reached.
- GitlabGetError: If the server fails to perform the request.
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabGetError: If the server failed to perform the request
+
+ Returns:
+ str: The blob content if streamed is False, None otherwise
"""
- 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)
+ path = '/projects/%s/repository/blobs/%s/raw' % (self.get_id(), sha)
+ result = self.manager.gitlab.http_get(path, streamed=streamed,
+ **kwargs)
+ return utils.response_content(result, streamed, action, chunk_size)
+ @exc.on_http_error(exc.GitlabGetError)
def repository_compare(self, from_, to, **kwargs):
- """Returns a diff between two branches/commits.
+ """Return a diff between two branches/commits.
Args:
- from_(str): orig branch/SHA
- to(str): dest branch/SHA
+ from_(str): Source branch/SHA
+ to(str): Destination branch/SHA
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabGetError: If the server failed to perform the request
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()
+ path = '/projects/%s/repository/compare' % self.get_id()
+ query_data = {'from': from_, 'to': to}
+ return self.manager.gitlab.http_get(path, query_data=query_data,
+ **kwargs)
- def repository_contributors(self):
- """Returns a list of contributors for the project.
+ @exc.on_http_error(exc.GitlabGetError)
+ def repository_contributors(self, **kwargs):
+ """Return a list of contributors for the project.
- Returns:
- list: The contibutors
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
Raises:
- GitlabConnectionError: If the server cannot be reached.
- GitlabGetError: If the server fails to perform the request.
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabGetError: If the server failed to perform the request
+
+ Returns:
+ list: The contributors
"""
- url = "/projects/%s/repository/contributors" % self.id
- r = self.gitlab._raw_get(url)
- raise_error_from_response(r, GitlabListError)
- return r.json()
+ path = '/projects/%s/repository/contributors' % self.get_id()
+ return self.manager.gitlab.http_get(path, **kwargs)
+ @exc.on_http_error(exc.GitlabListError)
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).
+ 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.
+ 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.
+ data
+ chunk_size (int): Size of each chunk
+ **kwargs: Extra options to send to the server (e.g. sudo)
Raises:
- GitlabConnectionError: If the server cannot be reached.
- GitlabGetError: If the server fails to perform the request.
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabListError: If the server failed to perform the request
+
+ Returns:
+ str: The binary data of the archive
"""
- url = '/projects/%s/repository/archive' % self.id
+ path = '/projects/%s/repository/archive' % self.get_id()
+ query_data = {}
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)
+ query_data['sha'] = sha
+ result = self.manager.gitlab.http_get(path, query_data=query_data,
+ streamed=streamed, **kwargs)
+ return utils.response_content(result, streamed, action, chunk_size)
- def create_fork_relation(self, forked_from_id):
+ @exc.on_http_error(exc.GitlabCreateError)
+ def create_fork_relation(self, forked_from_id, **kwargs):
"""Create a forked from/to relation between existing projects.
Args:
forked_from_id (int): The ID of the project that was forked from
+ **kwargs: Extra options to send to the server (e.g. sudo)
Raises:
- GitlabConnectionError: If the server cannot be reached.
- GitlabCreateError: If the server fails to perform the request.
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabCreateError: If the relation could not be created
"""
- url = "/projects/%s/fork/%s" % (self.id, forked_from_id)
- r = self.gitlab._raw_post(url)
- raise_error_from_response(r, GitlabCreateError, 201)
+ path = '/projects/%s/fork/%s' % (self.get_id(), forked_from_id)
+ self.manager.gitlab.http_post(path, **kwargs)
- def delete_fork_relation(self):
+ @exc.on_http_error(exc.GitlabDeleteError)
+ def delete_fork_relation(self, **kwargs):
"""Delete a forked relation between existing projects.
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
Raises:
- GitlabConnectionError: If the server cannot be reached.
- GitlabDeleteError: If the server fails to perform the request.
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabDeleteError: If the server failed to perform the request
"""
- url = "/projects/%s/fork" % self.id
- r = self.gitlab._raw_delete(url)
- raise_error_from_response(r, GitlabDeleteError)
+ path = '/projects/%s/fork' % self.get_id()
+ self.manager.gitlab.http_delete(path, **kwargs)
+ @exc.on_http_error(exc.GitlabCreateError)
def star(self, **kwargs):
"""Star a project.
- Returns:
- Project: the updated Project
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
Raises:
- GitlabCreateError: If the action cannot be done
- GitlabConnectionError: If the server cannot be reached.
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabCreateError: If the server failed to perform the request
"""
- 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
+ path = '/projects/%s/star' % self.get_id()
+ server_data = self.manager.gitlab.http_post(path, **kwargs)
+ self._update_attrs(server_data)
+ @exc.on_http_error(exc.GitlabDeleteError)
def unstar(self, **kwargs):
"""Unstar a project.
- Returns:
- Project: the updated Project
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
Raises:
- GitlabDeleteError: If the action cannot be done
- GitlabConnectionError: If the server cannot be reached.
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabDeleteError: If the server failed to perform the request
"""
- 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
+ path = '/projects/%s/unstar' % self.get_id()
+ server_data = self.manager.gitlab.http_post(path, **kwargs)
+ self._update_attrs(server_data)
+ @exc.on_http_error(exc.GitlabCreateError)
def archive(self, **kwargs):
"""Archive a project.
- Returns:
- Project: the updated Project
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
Raises:
- GitlabCreateError: If the action cannot be done
- GitlabConnectionError: If the server cannot be reached.
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabCreateError: If the server failed to perform the request
"""
- 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
+ path = '/projects/%s/archive' % self.get_id()
+ server_data = self.manager.gitlab.http_post(path, **kwargs)
+ self._update_attrs(server_data)
+ @exc.on_http_error(exc.GitlabDeleteError)
def unarchive(self, **kwargs):
"""Unarchive a project.
- Returns:
- Project: the updated Project
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
Raises:
- GitlabDeleteError: If the action cannot be done
- GitlabConnectionError: If the server cannot be reached.
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabDeleteError: If the server failed to perform the request
"""
- 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
+ path = '/projects/%s/unarchive' % self.get_id()
+ server_data = self.manager.gitlab.http_post(path, **kwargs)
+ self._update_attrs(server_data)
- def share(self, group_id, group_access, **kwargs):
+ @exc.on_http_error(exc.GitlabCreateError)
+ def share(self, group_id, group_access, expires_at=None, **kwargs):
"""Share the project with a group.
Args:
group_id (int): ID of the group.
group_access (int): Access level for the group.
+ **kwargs: Extra options to send to the server (e.g. sudo)
Raises:
- GitlabConnectionError: If the server cannot be reached.
- GitlabCreateError: If the server fails to perform the request.
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabCreateError: If the server failed 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)
+ path = '/projects/%s/share' % self.get_id()
+ data = {'group_id': group_id,
+ 'group_access': group_access,
+ 'expires_at': expires_at}
+ self.manager.gitlab.http_post(path, post_data=data, **kwargs)
+ @exc.on_http_error(exc.GitlabCreateError)
def trigger_pipeline(self, ref, token, variables={}, **kwargs):
"""Trigger a CI build.
@@ -2052,129 +1987,176 @@ class Project(GitlabObject):
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
+ **kwargs: Extra options to send to the server (e.g. sudo)
Raises:
- GitlabConnectionError: If the server cannot be reached.
- GitlabCreateError: If the server fails to perform the request.
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabCreateError: If the server failed to perform the request
"""
- url = "/projects/%s/trigger/pipeline" % self.id
+ path = '/projects/%s/trigger/pipeline' % self.get_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)
+ post_data = {'ref': ref, 'token': token}
+ post_data.update(form)
+ self.manager.gitlab.http_post(path, post_data=post_data, **kwargs)
-class Runner(GitlabObject):
- _url = '/runners'
- canCreate = False
- optionalUpdateAttrs = ['description', 'active', 'tag_list']
- optionalListAttrs = ['scope']
+class Runner(SaveMixin, ObjectDeleteMixin, RESTObject):
+ pass
-class RunnerManager(BaseManager):
- obj_cls = Runner
+class RunnerManager(RetrieveMixin, UpdateMixin, DeleteMixin, RESTManager):
+ _path = '/runners'
+ _obj_cls = Runner
+ _update_attrs = (tuple(), ('description', 'active', 'tag_list'))
+ _list_filters = ('scope', )
+ @exc.on_http_error(exc.GitlabListError)
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
+ all (bool): If True, return all the items, without pagination
+ per_page (int): Number of items to retrieve per request
+ page (int): ID of the page to return (starts with page 1)
+ as_list (bool): If set to False and no pagination option is
+ defined, return a generator instead of a list
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabListError: If the server failed to perform the request
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'
+ path = '/runners/all'
+ query_data = {}
if scope is not None:
- url += '?scope=' + scope
- return self.gitlab._raw_list(url, self.obj_cls, **kwargs)
+ query_data['scope'] = scope
+ return self.gitlab.http_list(path, query_data, **kwargs)
+
+class Todo(ObjectDeleteMixin, RESTObject):
+ @exc.on_http_error(exc.GitlabTodoError)
+ def mark_as_done(self, **kwargs):
+ """Mark the todo as done.
-class Todo(GitlabObject):
- _url = '/todos'
- canGet = 'from_list'
- canUpdate = False
- canCreate = False
- optionalListAttrs = ['action', 'author_id', 'project_id', 'state', 'type']
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabTodoError: If the server failed to perform the request
+ """
+ path = '%s/%s/mark_as_done' % (self.manager.path, self.id)
+ server_data = self.manager.gitlab.http_post(path, **kwargs)
+ self._update_attrs(server_data)
-class TodoManager(BaseManager):
- obj_cls = Todo
+class TodoManager(GetFromListMixin, DeleteMixin, RESTManager):
+ _path = '/todos'
+ _obj_cls = Todo
+ _list_filters = ('action', 'author_id', 'project_id', 'state', 'type')
- def delete_all(self, **kwargs):
+ @exc.on_http_error(exc.GitlabTodoError)
+ def mark_all_as_done(self, **kwargs):
"""Mark all the todos as done.
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
Raises:
- GitlabConnectionError: If the server cannot be reached.
- GitlabDeleteError: If the resource cannot be found
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabTodoError: If the server failed to perform the request
Returns:
- The number of todos maked done.
+ int: 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)
+ result = self.gitlab.http_post('/todos/mark_as_done', **kwargs)
+ try:
+ return int(result)
+ except ValueError:
+ return 0
+
+
+class ProjectManager(CRUDMixin, RESTManager):
+ _path = '/projects'
+ _obj_cls = Project
+ _create_attrs = (
+ ('name', ),
+ ('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')
+ )
+ _update_attrs = (
+ tuple(),
+ ('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')
+ )
+ _list_filters = ('search', 'owned', 'starred', 'archived', 'visibility',
+ 'order_by', 'sort', 'simple', 'membership', 'statistics',
+ 'with_issues_enabled', 'with_merge_requests_enabled')
+
+class GroupProject(Project):
+ pass
-class ProjectManager(BaseManager):
- obj_cls = Project
+class GroupProjectManager(GetFromListMixin, RESTManager):
+ _path = '/groups/%(group_id)s/projects'
+ _obj_cls = GroupProject
+ _from_parent_attrs = {'group_id': 'id'}
+ _list_filters = ('archived', 'visibility', 'order_by', 'sort', 'search',
+ 'ci_enabled_first')
-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')]),
+
+class Group(SaveMixin, ObjectDeleteMixin, RESTObject):
+ _short_print_attr = 'name'
+ _managers = (
+ ('accessrequests', 'GroupAccessRequestManager'),
+ ('members', 'GroupMemberManager'),
+ ('notificationsettings', 'GroupNotificationSettingsManager'),
+ ('projects', 'GroupProjectManager'),
+ ('issues', 'GroupIssueManager'),
)
+ @exc.on_http_error(exc.GitlabTransferProjectError)
def transfer_project(self, id, **kwargs):
- """Transfers a project to this new groups.
+ """Transfer a project to this group.
- Attrs:
- id (int): ID of the project to transfer.
+ Args:
+ id (int): ID of the project to transfer
+ **kwargs: Extra options to send to the server (e.g. sudo)
Raises:
- GitlabConnectionError: If the server cannot be reached.
- GitlabTransferProjectError: If the server fails to perform the
- request.
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabTransferProjectError: If the project could not be transfered
"""
- url = '/groups/%d/projects/%d' % (self.id, id)
- r = self.gitlab._raw_post(url, None, **kwargs)
- raise_error_from_response(r, GitlabTransferProjectError, 201)
+ path = '/groups/%d/projects/%d' % (self.id, id)
+ self.manager.gitlab.http_post(path, **kwargs)
-class GroupManager(BaseManager):
- obj_cls = Group
+class GroupManager(CRUDMixin, RESTManager):
+ _path = '/groups'
+ _obj_cls = Group
+ _create_attrs = (
+ ('name', 'path'),
+ ('description', 'visibility', 'parent_id', 'lfs_enabled',
+ 'request_access_enabled')
+ )
+ _update_attrs = (
+ tuple(),
+ ('name', 'path', 'description', 'visibility', 'lfs_enabled',
+ 'request_access_enabled')
+ )
diff --git a/tools/build_test_env.sh b/tools/build_test_env.sh
index 96d341a..35a54c6 100755
--- a/tools/build_test_env.sh
+++ b/tools/build_test_env.sh
@@ -154,6 +154,6 @@ log "Installing into virtualenv..."
try pip install -e .
log "Pausing to give GitLab some time to finish starting up..."
-sleep 20
+sleep 30
log "Test environment initialized."
diff --git a/tools/py_functional_tests.sh b/tools/py_functional_tests.sh
index 0d00c5f..75bb761 100755
--- a/tools/py_functional_tests.sh
+++ b/tools/py_functional_tests.sh
@@ -18,4 +18,4 @@ setenv_script=$(dirname "$0")/build_test_env.sh || exit 1
BUILD_TEST_ENV_AUTO_CLEANUP=true
. "$setenv_script" "$@" || exit 1
-try python "$(dirname "$0")"/python_test.py
+try python "$(dirname "$0")"/python_test_v${API_VER}.py
diff --git a/tools/python_test.py b/tools/python_test_v3.py
index 62d6421..a730f77 100644
--- a/tools/python_test.py
+++ b/tools/python_test_v3.py
@@ -55,15 +55,15 @@ foobar_user = gl.users.create(
{'email': 'foobar@example.com', 'username': 'foobar',
'name': 'Foo Bar', 'password': 'foobar_password'})
-assert gl.users.search('foobar') == [foobar_user]
+assert(gl.users.search('foobar')[0].id == foobar_user.id)
usercmp = lambda x,y: cmp(x.id, y.id)
expected = sorted([new_user, foobar_user], cmp=usercmp)
actual = sorted(gl.users.search('foo'), cmp=usercmp)
-assert expected == actual
-assert gl.users.search('asdf') == []
+assert len(expected) == len(actual)
+assert len(gl.users.search('asdf')) == 0
-assert gl.users.get_by_username('foobar') == foobar_user
-assert gl.users.get_by_username('foo') == new_user
+assert gl.users.get_by_username('foobar').id == foobar_user.id
+assert gl.users.get_by_username('foo').id == new_user.id
try:
gl.users.get_by_username('asdf')
except gitlab.GitlabGetError:
diff --git a/tools/python_test_v4.py b/tools/python_test_v4.py
new file mode 100644
index 0000000..cba4833
--- /dev/null
+++ b/tools/python_test_v4.py
@@ -0,0 +1,341 @@
+import base64
+import time
+
+import gitlab
+
+LOGIN = 'root'
+PASSWORD = '5iveL!fe'
+
+SSH_KEY = ("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDZAjAX8vTiHD7Yi3/EzuVaDChtih"
+ "79HyJZ6H9dEqxFfmGA1YnncE0xujQ64TCebhkYJKzmTJCImSVkOu9C4hZgsw6eE76n"
+ "+Cg3VwEeDUFy+GXlEJWlHaEyc3HWioxgOALbUp3rOezNh+d8BDwwqvENGoePEBsz5l"
+ "a6WP5lTi/HJIjAl6Hu+zHgdj1XVExeH+S52EwpZf/ylTJub0Bl5gHwf/siVE48mLMI"
+ "sqrukXTZ6Zg+8EHAIvIQwJ1dKcXe8P5IoLT7VKrbkgAnolS0I8J+uH7KtErZJb5oZh"
+ "S4OEwsNpaXMAr+6/wWSpircV2/e7sFLlhlKBC4Iq1MpqlZ7G3p foo@bar")
+DEPLOY_KEY = ("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFdRyjJQh+1niBpXqE2I8dzjG"
+ "MXFHlRjX9yk/UfOn075IdaockdU58sw2Ai1XIWFpZpfJkW7z+P47ZNSqm1gzeXI"
+ "rtKa9ZUp8A7SZe8vH4XVn7kh7bwWCUirqtn8El9XdqfkzOs/+FuViriUWoJVpA6"
+ "WZsDNaqINFKIA5fj/q8XQw+BcS92L09QJg9oVUuH0VVwNYbU2M2IRmSpybgC/gu"
+ "uWTrnCDMmLItksATifLvRZwgdI8dr+q6tbxbZknNcgEPrI2jT0hYN9ZcjNeWuyv"
+ "rke9IepE7SPBT41C+YtUX4dfDZDmczM1cE0YL/krdUCfuZHMa4ZS2YyNd6slufc"
+ "vn bar@foo")
+
+# login/password authentication
+gl = gitlab.Gitlab('http://localhost:8080', email=LOGIN, password=PASSWORD)
+gl.auth()
+token_from_auth = gl.private_token
+
+# token authentication from config file
+gl = gitlab.Gitlab.from_config(config_files=['/tmp/python-gitlab.cfg'])
+assert(token_from_auth == gl.private_token)
+gl.auth()
+assert(isinstance(gl.user, gitlab.v4.objects.CurrentUser))
+
+# settings
+settings = gl.settings.get()
+settings.default_projects_limit = 42
+settings.save()
+settings = gl.settings.get()
+assert(settings.default_projects_limit == 42)
+
+# user manipulations
+new_user = gl.users.create({'email': 'foo@bar.com', 'username': 'foo',
+ 'name': 'foo', 'password': 'foo_password'})
+users_list = gl.users.list()
+for user in users_list:
+ if user.username == 'foo':
+ break
+assert(new_user.username == user.username)
+assert(new_user.email == user.email)
+
+new_user.block()
+new_user.unblock()
+
+foobar_user = gl.users.create(
+ {'email': 'foobar@example.com', 'username': 'foobar',
+ 'name': 'Foo Bar', 'password': 'foobar_password'})
+
+assert gl.users.list(search='foobar')[0].id == foobar_user.id
+usercmp = lambda x,y: cmp(x.id, y.id)
+expected = sorted([new_user, foobar_user], cmp=usercmp)
+actual = sorted(list(gl.users.list(search='foo')), cmp=usercmp)
+assert len(expected) == len(actual)
+assert len(gl.users.list(search='asdf')) == 0
+
+# SSH keys
+key = new_user.keys.create({'title': 'testkey', 'key': SSH_KEY})
+assert(len(new_user.keys.list()) == 1)
+key.delete()
+assert(len(new_user.keys.list()) == 0)
+
+# emails
+email = new_user.emails.create({'email': 'foo2@bar.com'})
+assert(len(new_user.emails.list()) == 1)
+email.delete()
+assert(len(new_user.emails.list()) == 0)
+
+new_user.delete()
+foobar_user.delete()
+assert(len(gl.users.list()) == 3)
+
+# current user key
+key = gl.user.keys.create({'title': 'testkey', 'key': SSH_KEY})
+assert(len(gl.user.keys.list()) == 1)
+key.delete()
+assert(len(gl.user.keys.list()) == 0)
+
+# groups
+user1 = gl.users.create({'email': 'user1@test.com', 'username': 'user1',
+ 'name': 'user1', 'password': 'user1_pass'})
+user2 = gl.users.create({'email': 'user2@test.com', 'username': 'user2',
+ 'name': 'user2', 'password': 'user2_pass'})
+group1 = gl.groups.create({'name': 'group1', 'path': 'group1'})
+group2 = gl.groups.create({'name': 'group2', 'path': 'group2'})
+
+p_id = gl.groups.list(search='group2')[0].id
+group3 = gl.groups.create({'name': 'group3', 'path': 'group3', 'parent_id': p_id})
+
+assert(len(gl.groups.list()) == 3)
+assert(len(gl.groups.list(search='1')) == 1)
+assert(group3.parent_id == p_id)
+
+group1.members.create({'access_level': gitlab.Group.OWNER_ACCESS,
+ 'user_id': user1.id})
+group1.members.create({'access_level': gitlab.Group.GUEST_ACCESS,
+ 'user_id': user2.id})
+
+group2.members.create({'access_level': gitlab.Group.OWNER_ACCESS,
+ 'user_id': user2.id})
+
+# Administrator belongs to the groups
+assert(len(group1.members.list()) == 3)
+assert(len(group2.members.list()) == 2)
+
+group1.members.delete(user1.id)
+assert(len(group1.members.list()) == 2)
+member = group1.members.get(user2.id)
+member.access_level = gitlab.Group.OWNER_ACCESS
+member.save()
+member = group1.members.get(user2.id)
+assert(member.access_level == gitlab.Group.OWNER_ACCESS)
+
+group2.members.delete(gl.user.id)
+
+# hooks
+hook = gl.hooks.create({'url': 'http://whatever.com'})
+assert(len(gl.hooks.list()) == 1)
+hook.delete()
+assert(len(gl.hooks.list()) == 0)
+
+# projects
+admin_project = gl.projects.create({'name': 'admin_project'})
+gr1_project = gl.projects.create({'name': 'gr1_project',
+ 'namespace_id': group1.id})
+gr2_project = gl.projects.create({'name': 'gr2_project',
+ 'namespace_id': group2.id})
+sudo_project = gl.projects.create({'name': 'sudo_project'}, sudo=user1.name)
+
+assert(len(gl.projects.list(owned=True)) == 2)
+assert(len(gl.projects.list(search="admin")) == 1)
+
+# test pagination
+l1 = gl.projects.list(per_page=1, page=1)
+l2 = gl.projects.list(per_page=1, page=2)
+assert(len(l1) == 1)
+assert(len(l2) == 1)
+assert(l1[0].id != l2[0].id)
+
+# project content (files)
+admin_project.files.create({'file_path': 'README',
+ 'branch': 'master',
+ 'content': 'Initial content',
+ 'commit_message': 'Initial commit'})
+readme = admin_project.files.get(file_path='README', ref='master')
+readme.content = base64.b64encode("Improved README")
+time.sleep(2)
+readme.save(branch="master", commit_message="new commit")
+readme.delete(commit_message="Removing README", branch="master")
+
+admin_project.files.create({'file_path': 'README.rst',
+ 'branch': 'master',
+ 'content': 'Initial content',
+ 'commit_message': 'New commit'})
+readme = admin_project.files.get(file_path='README.rst', ref='master')
+assert(readme.decode() == 'Initial content')
+
+data = {
+ 'branch': 'master',
+ 'commit_message': 'blah blah blah',
+ 'actions': [
+ {
+ 'action': 'create',
+ 'file_path': 'blah',
+ 'content': 'blah'
+ }
+ ]
+}
+admin_project.commits.create(data)
+
+tree = admin_project.repository_tree()
+assert(len(tree) == 2)
+assert(tree[0]['name'] == 'README.rst')
+blob_id = tree[0]['id']
+blob = admin_project.repository_raw_blob(blob_id)
+assert(blob == 'Initial content')
+archive1 = admin_project.repository_archive()
+archive2 = admin_project.repository_archive('master')
+assert(archive1 == archive2)
+
+# deploy keys
+deploy_key = admin_project.keys.create({'title': 'foo@bar', 'key': DEPLOY_KEY})
+project_keys = list(admin_project.keys.list())
+assert(len(project_keys) == 1)
+
+sudo_project.keys.enable(deploy_key.id)
+assert(len(sudo_project.keys.list()) == 1)
+sudo_project.keys.delete(deploy_key.id)
+assert(len(sudo_project.keys.list()) == 0)
+
+# labels
+label1 = admin_project.labels.create({'name': 'label1', 'color': '#778899'})
+label1 = admin_project.labels.get('label1')
+assert(len(admin_project.labels.list()) == 1)
+label1.new_name = 'label1updated'
+label1.save()
+assert(label1.name == 'label1updated')
+label1.subscribe()
+assert(label1.subscribed == True)
+label1.unsubscribe()
+assert(label1.subscribed == False)
+label1.delete()
+
+# milestones
+m1 = admin_project.milestones.create({'title': 'milestone1'})
+assert(len(admin_project.milestones.list()) == 1)
+m1.due_date = '2020-01-01T00:00:00Z'
+m1.save()
+m1.state_event = 'close'
+m1.save()
+m1 = admin_project.milestones.get(1)
+assert(m1.state == 'closed')
+
+# issues
+issue1 = admin_project.issues.create({'title': 'my issue 1',
+ 'milestone_id': m1.id})
+issue2 = admin_project.issues.create({'title': 'my issue 2'})
+issue3 = admin_project.issues.create({'title': 'my issue 3'})
+assert(len(admin_project.issues.list()) == 3)
+issue3.state_event = 'close'
+issue3.save()
+assert(len(admin_project.issues.list(state='closed')) == 1)
+assert(len(admin_project.issues.list(state='opened')) == 2)
+assert(len(admin_project.issues.list(milestone='milestone1')) == 1)
+assert(m1.issues().next().title == 'my issue 1')
+
+# tags
+tag1 = admin_project.tags.create({'tag_name': 'v1.0', 'ref': 'master'})
+assert(len(admin_project.tags.list()) == 1)
+tag1.set_release_description('Description 1')
+tag1.set_release_description('Description 2')
+assert(tag1.release['description'] == 'Description 2')
+tag1.delete()
+
+# triggers
+tr1 = admin_project.triggers.create({'description': 'trigger1'})
+assert(len(admin_project.triggers.list()) == 1)
+tr1.delete()
+
+# variables
+v1 = admin_project.variables.create({'key': 'key1', 'value': 'value1'})
+assert(len(admin_project.variables.list()) == 1)
+v1.value = 'new_value1'
+v1.save()
+v1 = admin_project.variables.get(v1.key)
+assert(v1.value == 'new_value1')
+v1.delete()
+
+# branches and merges
+to_merge = admin_project.branches.create({'branch': 'branch1',
+ 'ref': 'master'})
+admin_project.files.create({'file_path': 'README2.rst',
+ 'branch': 'branch1',
+ 'content': 'Initial content',
+ 'commit_message': 'New commit in new branch'})
+mr = admin_project.mergerequests.create({'source_branch': 'branch1',
+ 'target_branch': 'master',
+ 'title': 'MR readme2'})
+mr.merge()
+admin_project.branches.delete('branch1')
+
+try:
+ mr.merge()
+except gitlab.GitlabMRClosedError:
+ pass
+
+# stars
+admin_project.star()
+assert(admin_project.star_count == 1)
+admin_project.unstar()
+assert(admin_project.star_count == 0)
+
+# project boards
+#boards = admin_project.boards.list()
+#assert(len(boards))
+#board = boards[0]
+#lists = board.lists.list()
+#begin_size = len(lists)
+#last_list = lists[-1]
+#last_list.position = 0
+#last_list.save()
+#last_list.delete()
+#lists = board.lists.list()
+#assert(len(lists) == begin_size - 1)
+
+# namespaces
+ns = gl.namespaces.list(all=True)
+assert(len(ns) != 0)
+ns = gl.namespaces.list(search='root', all=True)[0]
+assert(ns.kind == 'user')
+
+# broadcast messages
+msg = gl.broadcastmessages.create({'message': 'this is the message'})
+msg.color = '#444444'
+msg.save()
+msg = gl.broadcastmessages.list(all=True)[0]
+assert(msg.color == '#444444')
+msg = gl.broadcastmessages.get(1)
+assert(msg.color == '#444444')
+msg.delete()
+assert(len(gl.broadcastmessages.list()) == 0)
+
+# notification settings
+settings = gl.notificationsettings.get()
+settings.level = gitlab.NOTIFICATION_LEVEL_WATCH
+settings.save()
+settings = gl.notificationsettings.get()
+assert(settings.level == gitlab.NOTIFICATION_LEVEL_WATCH)
+
+# services
+service = admin_project.services.get('asana')
+service.api_key = 'whatever'
+service.save()
+service = admin_project.services.get('asana')
+assert(service.active == True)
+service.delete()
+service = admin_project.services.get('asana')
+assert(service.active == False)
+
+# snippets
+snippets = gl.snippets.list(all=True)
+assert(len(snippets) == 0)
+snippet = gl.snippets.create({'title': 'snippet1', 'file_name': 'snippet1.py',
+ 'content': 'import gitlab'})
+snippet = gl.snippets.get(1)
+snippet.title = 'updated_title'
+snippet.save()
+snippet = gl.snippets.get(1)
+assert(snippet.title == 'updated_title')
+content = snippet.content()
+assert(content == 'import gitlab')
+snippet.delete()
+assert(len(gl.snippets.list()) == 0)