diff options
| author | Gauvain Pocentek <gauvain.pocentek@objectif-libre.com> | 2015-05-11 19:51:21 +0200 |
|---|---|---|
| committer | Gauvain Pocentek <gauvain.pocentek@objectif-libre.com> | 2015-05-11 19:56:21 +0200 |
| commit | d2e5700c68f33b0872616fedc6a3320c84c81de6 (patch) | |
| tree | ccc04bca14c092f6d14eeaaf4db81a5dda1f8da8 /gitlab | |
| parent | 105896f59bd3399c7d76934e515dab57bcd4f594 (diff) | |
| download | gitlab-d2e5700c68f33b0872616fedc6a3320c84c81de6.tar.gz | |
gitlab is now a package
Diffstat (limited to 'gitlab')
| -rwxr-xr-x | gitlab | 363 | ||||
| -rw-r--r-- | gitlab/__init__.py | 1297 |
2 files changed, 1297 insertions, 363 deletions
@@ -1,363 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# -# Copyright (C) 2013-2014 Gauvain Pocentek <gauvain@pocentek.net> -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -from __future__ import print_function -from __future__ import division -from __future__ import absolute_import -import argparse -from inspect import getmro -import os -import re -import sys -try: - from ConfigParser import ConfigParser -except ImportError: - from configparser import ConfigParser - -import gitlab - -camel_re = re.compile('(.)([A-Z])') -LIST = 'list' -GET = 'get' -CREATE = 'create' -UPDATE = 'update' -DELETE = 'delete' -PROTECT = 'protect' -UNPROTECT = 'unprotect' -SEARCH = 'search' -OWNED = 'owned' -ALL = 'all' -ACTIONS = [LIST, GET, CREATE, UPDATE, DELETE] -EXTRA_ACTION = [PROTECT, UNPROTECT, SEARCH, OWNED, ALL] - -extra_actions = { - gitlab.ProjectBranch: {PROTECT: {'requiredAttrs': ['id', 'project-id']}, - UNPROTECT: {'requiredAttrs': ['id', 'project-id']}}, - gitlab.Project: {SEARCH: {'requiredAttrs': ['query']}, - OWNED: {'requiredAttrs': []}, - ALL: {'requiredAttrs': []}}, -} - - -def die(msg): - sys.stderr.write(msg + "\n") - sys.exit(1) - - -def whatToCls(what): - return "".join([s.capitalize() for s in what.split("-")]) - - -def clsToWhat(cls): - return camel_re.sub(r'\1-\2', cls.__name__).lower() - - -def populate_sub_parser_by_class(cls, sub_parser): - sub_parser_class = sub_parser.add_subparsers( - dest='action', - title="positional argument", - description='action with %s' % cls.__name__, - help='action to do' - ) - for action_name in ACTIONS: - attr = 'can' + action_name.capitalize() - try: - y = cls.__dict__[attr] - except AttributeError: - y = gitlab.GitlabObject.__dict__[attr] - if not y: - continue - sub_parser_action = sub_parser_class.add_parser(action_name) - [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), - required=True) - for x in cls.requiredUrlAttrs] - - 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) - - elif action_name in [GET, DELETE]: - if cls not in [gitlab.CurrentUser]: - sub_parser_action.add_argument("--id", required=True) - [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), - required=True) - for x in cls.requiredGetAttrs] - - elif 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] - - elif action_name == UPDATE: - [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 cls in extra_actions: - for action_name in sorted(extra_actions[cls]): - sub_parser_action = sub_parser_class.add_parser(action_name) - d = extra_actions[cls][action_name] - [sub_parser_action.add_argument("--%s" % arg, required=True) - for arg in d['requiredAttrs']] - - -def do_auth(): - try: - gl = gitlab.Gitlab(gitlab_url, private_token=gitlab_token, - ssl_verify=ssl_verify, timeout=timeout) - gl.auth() - return gl - except Exception as e: - die("Could not connect to GitLab %s (%s)" % (gitlab_url, str(e))) - - -def get_id(): - try: - id = d.pop('id') - except Exception: - die("Missing --id argument") - - return id - - -def do_create(cls, d): - if not cls.canCreate: - die("%s objects can't be created" % what) - - try: - o = cls(gl, d) - o.save() - except Exception as e: - die("Impossible to create object (%s)" % str(e)) - - return o - - -def do_list(cls, d): - if not cls.canList: - die("%s objects can't be listed" % what) - - try: - l = cls.list(gl, **d) - except Exception as e: - die("Impossible to list objects (%s)" % str(e)) - - return l - - -def do_get(cls, d): - if not cls.canGet: - die("%s objects can't be retrieved" % what) - - id = None - if cls not in [gitlab.CurrentUser]: - id = get_id() - - try: - o = cls(gl, id, **d) - except Exception as e: - die("Impossible to get object (%s)" % str(e)) - - return o - - -def do_delete(cls, d): - if not cls.canDelete: - die("%s objects can't be deleted" % what) - - o = do_get(cls, d) - try: - o.delete() - except Exception as e: - die("Impossible to destroy object (%s)" % str(e)) - - -def do_update(cls, d): - if not cls.canUpdate: - die("%s objects can't be updated" % what) - - o = do_get(cls, d) - try: - for k, v in d.items(): - o.__dict__[k] = v - o.save() - except Exception as e: - die("Impossible to update object (%s)" % str(e)) - - return o - - -def do_project_search(d): - try: - return gl.search_projects(d['query']) - except Exception as e: - die("Impossible to search projects (%s)" % str(e)) - - -def do_project_all(): - try: - return gl.all_projects() - except Exception as e: - die("Impossible to list all projects (%s)" % str(e)) - - -def do_project_owned(): - try: - return gl.owned_projects() - except Exception as e: - die("Impossible to list owned projects (%s)" % str(e)) - - -if __name__ == "__main__": - ssl_verify = True - timeout = 60 - parser = argparse.ArgumentParser( - description='GitLab API Command Line Interface') - parser.add_argument("-v", "--verbosity", "--fancy", - help="increase output verbosity", - action="store_true") - parser.add_argument("--gitlab", metavar='gitlab', - help=("Specifies which python-gitlab.cfg " - "configuration section should be used. " - "If not defined, the default selection " - "will be used."), - required=False) - subparsers = parser.add_subparsers( - dest='what', - title="positional argument", - description='GitLab object', - help='GitLab object' - ) - - # populate argparse for all Gitlab Object - for cls in gitlab.__dict__.values(): - try: - if gitlab.GitlabObject in getmro(cls): - sub_parser = subparsers.add_parser(clsToWhat(cls)) - populate_sub_parser_by_class(cls, sub_parser) - except Exception: - pass - - arg = parser.parse_args() - d = arg.__dict__ - - # read the config - config = ConfigParser() - config.read(['/etc/python-gitlab.cfg', - os.path.expanduser('~/.python-gitlab.cfg')]) - gitlab_id = arg.gitlab - # conflicts with "gitlab" attribute from GitlabObject class - d.pop("gitlab") - verbose = arg.verbosity - action = arg.action - what = arg.what - - if gitlab_id is None: - try: - gitlab_id = config.get('global', 'default') - except Exception: - die("Impossible to get the gitlab id " - "(not specified in config file)") - - try: - gitlab_url = config.get(gitlab_id, 'url') - gitlab_token = config.get(gitlab_id, 'private_token') - except Exception: - die("Impossible to get gitlab informations from configuration " - "(%s)" % gitlab_id) - - try: - ssl_verify = config.getboolean('global', 'ssl_verify') - except Exception: - pass - try: - ssl_verify = config.getboolean(gitlab_id, 'ssl_verify') - except Exception: - pass - - try: - timeout = config.getint('global', 'timeout') - except Exception: - pass - try: - timeout = config.getint(gitlab_id, 'timeout') - except Exception: - pass - - cls = None - try: - cls = gitlab.__dict__[whatToCls(what)] - except Exception: - die("Unknown object: %s" % what) - - gl = do_auth() - - if action == CREATE or action == GET: - o = globals()['do_%s' % action.lower()](cls, d) - o.display(verbose) - - elif action == LIST: - for o in do_list(cls, d): - o.display(verbose) - print("") - - elif action == DELETE or action == UPDATE: - o = globals()['do_%s' % action.lower()](cls, d) - - elif action == PROTECT or action == UNPROTECT: - if cls != gitlab.ProjectBranch: - die("%s objects can't be protected" % what) - - o = do_get(cls, d) - getattr(o, action)() - - elif action == SEARCH: - if cls != gitlab.Project: - die("%s objects don't support this request" % what) - - for o in do_project_search(d): - o.display(verbose) - - elif action == OWNED: - if cls != gitlab.Project: - die("%s objects don't support this request" % what) - - for o in do_project_owned(): - o.display(verbose) - - elif action == ALL: - if cls != gitlab.Project: - die("%s objects don't support this request" % what) - - for o in do_project_all(): - o.display(verbose) - - else: - die("Unknown action: %s. Use \"gitlab -h %s\" to get details." % - (action, what)) - - sys.exit(0) diff --git a/gitlab/__init__.py b/gitlab/__init__.py new file mode 100644 index 0000000..3466127 --- /dev/null +++ b/gitlab/__init__.py @@ -0,0 +1,1297 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2013-2014 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/>. +"""Package for interfacing with GitLab-api """ +from __future__ import print_function +from __future__ import division +from __future__ import absolute_import +from itertools import chain +import json +import sys + +import requests +import six + +__title__ = 'python-gitlab' +__version__ = '0.8.1' +__author__ = 'Gauvain Pocentek' +__email__ = 'gauvain@pocentek.net' +__license__ = 'LGPL3' +__copyright__ = 'Copyright 2013-2014 Gauvain Pocentek' + + +class jsonEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, GitlabObject): + return obj.__dict__ + elif isinstance(obj, Gitlab): + return {'url': obj._url} + return json.JSONEncoder.default(self, obj) + + +class GitlabError(Exception): + def __init__(self, error_message="", response_code=None, + response_body=None): + + Exception.__init__(self, error_message) + # Http status code + self.response_code = response_code + # Full http response + self.response_body = response_body + # Parsed error message from gitlab + self.error_message = error_message + + def __str__(self): + if self.response_code is not None: + return "{0}: {1}".format(self.response_code, self.error_message) + else: + return "{0}".format(self.error_message) + + +class GitlabAuthenticationError(GitlabError): + pass + + +class GitlabConnectionError(GitlabError): + pass + + +class GitlabOperationError(GitlabError): + pass + + +class GitlabListError(GitlabOperationError): + pass + + +class GitlabGetError(GitlabOperationError): + pass + + +class GitlabCreateError(GitlabOperationError): + pass + + +class GitlabUpdateError(GitlabOperationError): + pass + + +class GitlabDeleteError(GitlabOperationError): + pass + + +class GitlabProtectError(GitlabOperationError): + pass + + +class GitlabTransferProjectError(GitlabOperationError): + pass + + +def _raiseErrorFromResponse(response, error): + """Tries to parse gitlab error message from response and raises error. + + If response status code is 401, raises instead GitlabAuthenticationError. + + response: requests response object + error: Error-class to raise. Should be inherited from GitLabError + """ + + try: + message = response.json()['message'] + except (KeyError, ValueError): + message = response.content + + if response.status_code == 401: + error = GitlabAuthenticationError + + raise error(error_message=message, + response_code=response.status_code, + response_body=response.content) + + +class Gitlab(object): + """Represents a GitLab server connection + + Args: + url (str): the URL of the Gitlab server + private_token (str): the user private token + email (str): the user email/login + password (str): the user password (associated with email) + ssl_verify (bool): (Passed to requests-library) + timeout (float or tuple(float,float)): (Passed to + requests-library). Timeout to use for requests to gitlab server + """ + + def __init__(self, url, private_token=None, + email=None, password=None, ssl_verify=True, timeout=None): + + self._url = '%s/api/v3' % url + #: Timeout to use for requests to gitlab server + self.timeout = timeout + #: Headers that will be used in request to GitLab + self.headers = {} + self.setToken(private_token) + #: the user email + self.email = email + #: the user password (associated with email) + self.password = password + #: (Passed to requests-library) + self.ssl_verify = ssl_verify + + def auth(self): + """Performs an authentication. + + Uses either the private token, or the email/password pair. + + The user attribute will hold a CurrentUser object on success. + """ + if self.private_token: + self.token_auth() + else: + self.credentials_auth() + + def credentials_auth(self): + if not self.email or not self.password: + raise GitlabAuthenticationError("Missing email/password") + + data = json.dumps({'email': self.email, 'password': self.password}) + r = self.rawPost('/session', data, content_type='application/json') + + if r.status_code == 201: + self.user = CurrentUser(self, r.json()) + else: + _raiseErrorFromResponse(r, GitlabAuthenticationError) + + self.setToken(self.user.private_token) + + def token_auth(self): + self.user = CurrentUser(self) + + def setUrl(self, url): + """Updates the gitlab URL""" + self._url = '%s/api/v3' % url + + def constructUrl(self, id_, obj, parameters): + args = _sanitize_dict(parameters) + url = obj._url % args + if id_ is not None: + url = '%s%s/%s' % (self._url, url, str(id_)) + else: + url = '%s%s' % (self._url, url) + return url + + def _createHeaders(self, content_type=None, headers={}): + request_headers = self.headers.copy() + request_headers.update(headers) + if content_type is not None: + request_headers['Content-type'] = content_type + return request_headers + + def setToken(self, token): + """Sets the private token for authentication""" + self.private_token = token if token else None + if token: + self.headers["PRIVATE-TOKEN"] = token + elif "PRIVATE-TOKEN" in self.headers: + del self.headers["PRIVATE-TOKEN"] + + def setCredentials(self, email, password): + """Sets the email/login and password for authentication""" + self.email = email + self.password = password + + def rawGet(self, path, content_type=None, **kwargs): + url = '%s%s' % (self._url, path) + headers = self._createHeaders(content_type) + + try: + return requests.get(url, + params=kwargs, + headers=headers, + verify=self.ssl_verify, + timeout=self.timeout) + except Exception: + raise GitlabConnectionError( + "Can't connect to GitLab server (%s)" % self._url) + + def rawPost(self, path, data=None, content_type=None, **kwargs): + url = '%s%s' % (self._url, path) + headers = self._createHeaders(content_type) + try: + return requests.post(url, params=kwargs, data=data, + headers=headers, + verify=self.ssl_verify, + timeout=self.timeout) + except Exception: + raise GitlabConnectionError( + "Can't connect to GitLab server (%s)" % self._url) + + def rawPut(self, path, data=None, content_type=None, **kwargs): + url = '%s%s' % (self._url, path) + headers = self._createHeaders(content_type) + + try: + return requests.put(url, data=data, params=kwargs, + headers=headers, + verify=self.ssl_verify, + timeout=self.timeout) + except Exception: + raise GitlabConnectionError( + "Can't connect to GitLab server (%s)" % self._url) + + def rawDelete(self, path, content_type=None, **kwargs): + url = '%s%s' % (self._url, path) + headers = self._createHeaders(content_type) + + try: + return requests.delete(url, + params=kwargs, + headers=headers, + verify=self.ssl_verify, + timeout=self.timeout) + except Exception: + raise GitlabConnectionError( + "Can't connect to GitLab server (%s)" % self._url) + + def list(self, obj_class, **kwargs): + missing = [] + for k in chain(obj_class.requiredUrlAttrs, + obj_class.requiredListAttrs): + if k not in kwargs: + missing.append(k) + if missing: + raise GitlabListError('Missing attribute(s): %s' % + ", ".join(missing)) + + url = self.constructUrl(id_=None, obj=obj_class, parameters=kwargs) + headers = self._createHeaders() + + # Remove attributes that are used in url so that there is only + # url-parameters left + params = kwargs.copy() + for attribute in obj_class.requiredUrlAttrs: + del params[attribute] + + try: + r = requests.get(url, params=params, headers=headers, + verify=self.ssl_verify, + timeout=self.timeout) + except Exception: + raise GitlabConnectionError( + "Can't connect to GitLab server (%s)" % self._url) + + if r.status_code == 200: + cls = obj_class + if obj_class._returnClass: + cls = obj_class._returnClass + + cls_kwargs = kwargs.copy() + + # Add _created manually, because we are not creating objects + # through normal path + cls_kwargs['_created'] = True + + # Remove parameters from kwargs before passing it to constructor + for key in ['page', 'per_page', 'sudo']: + if key in cls_kwargs: + del cls_kwargs[key] + + return [cls(self, item, **cls_kwargs) for item in r.json() + if item is not None] + else: + _raiseErrorFromResponse(r, GitlabListError) + + def get(self, obj_class, id=None, **kwargs): + missing = [] + for k in chain(obj_class.requiredUrlAttrs, + obj_class.requiredGetAttrs): + if k not in kwargs: + missing.append(k) + if missing: + raise GitlabGetError('Missing attribute(s): %s' % + ", ".join(missing)) + + url = self.constructUrl(id_=id, obj=obj_class, parameters=kwargs) + headers = self._createHeaders() + + # Remove attributes that are used in url so that there is only + # url-parameters left + params = kwargs.copy() + for attribute in obj_class.requiredUrlAttrs: + del params[attribute] + + try: + r = requests.get(url, params=params, headers=headers, + verify=self.ssl_verify, timeout=self.timeout) + except Exception: + raise GitlabConnectionError( + "Can't connect to GitLab server (%s)" % self._url) + + if r.status_code == 200: + return r.json() + else: + _raiseErrorFromResponse(r, GitlabGetError) + + def delete(self, obj, **kwargs): + params = obj.__dict__.copy() + params.update(kwargs) + missing = [] + for k in chain(obj.requiredUrlAttrs, obj.requiredDeleteAttrs): + if k not in params: + missing.append(k) + if missing: + raise GitlabDeleteError('Missing attribute(s): %s' % + ", ".join(missing)) + + url = self.constructUrl(id_=obj.id, obj=obj, parameters=params) + headers = self._createHeaders() + + # Remove attributes that are used in url so that there is only + # url-parameters left + for attribute in obj.requiredUrlAttrs: + del params[attribute] + + try: + r = requests.delete(url, + params=params, + headers=headers, + verify=self.ssl_verify, + timeout=self.timeout) + except Exception: + raise GitlabConnectionError( + "Can't connect to GitLab server (%s)" % self._url) + + if r.status_code == 200: + return True + else: + _raiseErrorFromResponse(r, GitlabDeleteError) + + def create(self, obj, **kwargs): + params = obj.__dict__.copy() + params.update(kwargs) + missing = [] + for k in chain(obj.requiredUrlAttrs, obj.requiredCreateAttrs): + if k not in params: + missing.append(k) + if missing: + raise GitlabCreateError('Missing attribute(s): %s' % + ", ".join(missing)) + + url = self.constructUrl(id_=None, obj=obj, parameters=params) + headers = self._createHeaders(content_type="application/json") + + # build data that can really be sent to server + data = obj._dataForGitlab(extra_parameters=kwargs) + + try: + r = requests.post(url, data=data, + headers=headers, + verify=self.ssl_verify, + timeout=self.timeout) + except Exception: + raise GitlabConnectionError( + "Can't connect to GitLab server (%s)" % self._url) + + if r.status_code == 201: + return r.json() + else: + _raiseErrorFromResponse(r, GitlabCreateError) + + def update(self, obj, **kwargs): + params = obj.__dict__.copy() + params.update(kwargs) + missing = [] + for k in chain(obj.requiredUrlAttrs, obj.requiredCreateAttrs): + if k not in params: + missing.append(k) + if missing: + raise GitlabUpdateError('Missing attribute(s): %s' % + ", ".join(missing)) + url = self.constructUrl(id_=obj.id, obj=obj, parameters=params) + headers = self._createHeaders(content_type="application/json") + + # build data that can really be sent to server + data = obj._dataForGitlab(extra_parameters=kwargs) + + try: + r = requests.put(url, data=data, + headers=headers, + verify=self.ssl_verify, + timeout=self.timeout) + except Exception: + raise GitlabConnectionError( + "Can't connect to GitLab server (%s)" % self._url) + + if r.status_code == 200: + return r.json() + else: + _raiseErrorFromResponse(r, GitlabUpdateError) + + def Hook(self, id=None, **kwargs): + """Creates/tests/lists system hook(s) known by the GitLab server. + + If id is None, returns a list of hooks. + + If id is an integer, tests the matching hook. + + If id is a dict, creates a new object using attributes provided. The + object is NOT saved on the server. Use the save() method on the object + to write it on the server. + """ + return Hook._getListOrObject(self, id, **kwargs) + + def Project(self, id=None, **kwargs): + """Creates/gets/lists project(s) known by the GitLab server. + + If id is None, returns a list of projects. + + If id is an integer, returns the matching project (or raises a + GitlabGetError if not found) + + If id is a dict, creates a new object using attributes provided. The + object is NOT saved on the server. Use the save() method on the object + to write it on the server. + """ + return Project._getListOrObject(self, id, **kwargs) + + def UserProject(self, id=None, **kwargs): + """Creates a project for a user. + + id must be a dict. + """ + return UserProject._getListOrObject(self, id, **kwargs) + + def _list_projects(self, url, **kwargs): + r = self.rawGet(url, **kwargs) + if r.status_code != 200: + _raiseErrorFromResponse(r, GitlabListError) + + l = [] + for o in r.json(): + l.append(Project(self, o)) + + return l + + def search_projects(self, query, **kwargs): + """Searches projects by name. + + Returns a list of matching projects. + """ + return self._list_projects("/projects/search/" + query, **kwargs) + + def all_projects(self, **kwargs): + """Lists all the projects (need admin rights).""" + return self._list_projects("/projects/all", **kwargs) + + def owned_projects(self, **kwargs): + """Lists owned projects.""" + return self._list_projects("/projects/owned", **kwargs) + + def Group(self, id=None, **kwargs): + """Creates/gets/lists group(s) known by the GitLab server + + Args: + id: If id is None, returns a list of groups. + id: If id is an integer, + returns the matching group (or raises a GitlabGetError if not + found). + id: If id is a dict, creates a new object using attributes + provided. The object is NOT saved on the server. Use the + save() method on the object to write it on the server. + kwargs: Arbitrary keyword arguments + """ + return Group._getListOrObject(self, id, **kwargs) + + def Issue(self, id=None, **kwargs): + """Lists issues(s) known by the GitLab server. + + Does not support creation or getting a single issue unlike other + methods in this class yet. + """ + return Issue._getListOrObject(self, id, **kwargs) + + def User(self, id=None, **kwargs): + """Creates/gets/lists users(s) known by the GitLab server. + + If id is None, returns a list of users. + + If id is an integer, returns the matching user (or raises a + GitlabGetError if not found) + + If id is a dict, creates a new object using attributes provided. The + object is NOT saved on the server. Use the save() method on the object + to write it on the server. + """ + return User._getListOrObject(self, id, **kwargs) + + def Team(self, id=None, **kwargs): + """Creates/gets/lists team(s) known by the GitLab server. + + If id is None, returns a list of teams. + + If id is an integer, returns the matching team (or raises a + GitlabGetError if not found) + + If id is a dict, create a new object using attributes provided. The + object is NOT saved on the server. Use the save() method on the object + to write it on the server. + """ + return Team._getListOrObject(self, id, **kwargs) + + +def _get_display_encoding(): + return sys.stdout.encoding or sys.getdefaultencoding() + + +def _sanitize(value): + if isinstance(value, six.string_types): + return value.replace('/', '%2F') + return value + + +def _sanitize_dict(src): + return dict((k, _sanitize(v)) for k, v in src.items()) + + +class GitlabObject(object): + """Base class for all classes that interface with GitLab + + Args: + gl (gitlab.Gitlab): GitLab server connection + data: If data is integer or string type, get object from GitLab + data: If data is dictionary, create new object locally. To save object + in GitLab, call save-method + kwargs: Arbitrary keyword arguments + """ + #: Url to use in GitLab for this object + _url = None + _returnClass = None + _constructorTypes = None + #: Tells if _getListOrObject should return list or object when id is None + getListWhenNoId = True + + #: Tells if GitLab-api allows retrieving single objects + canGet = True + #: Tells if GitLab-api allows listing of objects + canList = True + #: Tells if GitLab-api allows creation of new objects + canCreate = True + #: Tells if GitLab-api allows updating object + canUpdate = True + #: Tells if GitLab-api allows deleting object + canDelete = True + #: Attributes that are required for constructing url + requiredUrlAttrs = [] + #: Attributes that are required when retrieving list of objects + requiredListAttrs = [] + #: Attributes that are required when retrieving single object + requiredGetAttrs = [] + #: Attributes that are required when deleting object + requiredDeleteAttrs = [] + #: Attributes that are required when creating a new object + requiredCreateAttrs = [] + #: Attributes that are optional when creating a new object + optionalCreateAttrs = [] + idAttr = 'id' + shortPrintAttr = None + + def _dataForGitlab(self, extra_parameters={}): + data = {} + for attribute in chain(self.requiredCreateAttrs, + self.optionalCreateAttrs): + if hasattr(self, attribute): + data[attribute] = getattr(self, attribute) + + data.update(extra_parameters) + + return json.dumps(data) + + @classmethod + def list(cls, gl, **kwargs): + if not cls.canList: + raise NotImplementedError + + if not cls._url: + raise NotImplementedError + + return gl.list(cls, **kwargs) + + @classmethod + def _getListOrObject(cls, gl, id, **kwargs): + if id is None and cls.getListWhenNoId: + return cls.list(gl, **kwargs) + else: + return cls(gl, id, **kwargs) + + def _getObject(self, k, v): + if self._constructorTypes and k in self._constructorTypes: + return globals()[self._constructorTypes[k]](self.gitlab, v) + else: + return v + + def _setFromDict(self, data): + for k, v in data.items(): + if isinstance(v, list): + self.__dict__[k] = [] + for i in v: + self.__dict__[k].append(self._getObject(k, i)) + elif v is None: + self.__dict__[k] = None + else: + self.__dict__[k] = self._getObject(k, v) + + def _create(self, **kwargs): + if not self.canCreate: + raise NotImplementedError + + json = self.gitlab.create(self, **kwargs) + self._setFromDict(json) + self._created = True + + def _update(self, **kwargs): + if not self.canUpdate: + raise NotImplementedError + + json = self.gitlab.update(self, **kwargs) + self._setFromDict(json) + + def save(self, **kwargs): + if self._created: + self._update(**kwargs) + else: + self._create(**kwargs) + + def delete(self, **kwargs): + if not self.canDelete: + raise NotImplementedError + + if not self._created: + raise GitlabDeleteError("Object not yet created") + + return self.gitlab.delete(self, **kwargs) + + def __init__(self, gl, data=None, **kwargs): + self._created = False + self.gitlab = gl + + if data is None or isinstance(data, six.integer_types) or\ + isinstance(data, six.string_types): + if not self.canGet: + raise NotImplementedError + data = self.gitlab.get(self.__class__, data, **kwargs) + # Object is created because we got it from api + self._created = True + + self._setFromDict(data) + + if kwargs: + for k, v in kwargs.items(): + self.__dict__[k] = v + + # Special handling for api-objects that don't have id-number in api + # responses. Currently only Labels and Files + if not hasattr(self, "id"): + self.id = None + + def __str__(self): + return '%s => %s' % (type(self), str(self.__dict__)) + + def display(self, pretty): + if pretty: + self.pretty_print() + else: + self.short_print() + + def short_print(self, depth=0): + id = self.__dict__[self.idAttr] + print("%s%s: %s" % (" " * depth * 2, self.idAttr, id)) + if self.shortPrintAttr: + print("%s%s: %s" % (" " * depth * 2, + self.shortPrintAttr.replace('_', '-'), + self.__dict__[self.shortPrintAttr])) + + @staticmethod + def _obj_to_str(obj): + if isinstance(obj, dict): + s = ", ".join(["%s: %s" % + (x, GitlabObject._obj_to_str(y)) + for (x, y) in obj.items()]) + return "{ %s }" % s + elif isinstance(obj, list): + s = ", ".join([GitlabObject._obj_to_str(x) for x in obj]) + return "[ %s ]" % s + elif six.PY2 and isinstance(obj, six.text_type): + return obj.encode(_get_display_encoding(), "replace") + else: + return str(obj) + + def pretty_print(self, depth=0): + id = self.__dict__[self.idAttr] + print("%s%s: %s" % (" " * depth * 2, self.idAttr, id)) + for k in sorted(self.__dict__.keys()): + if k == self.idAttr or k == 'id': + continue + if k[0] == '_': + continue + v = self.__dict__[k] + pretty_k = k.replace('_', '-') + if six.PY2: + pretty_k = pretty_k.encode(_get_display_encoding(), "replace") + if isinstance(v, GitlabObject): + if depth == 0: + print("%s:" % pretty_k) + v.pretty_print(1) + else: + print("%s: %s" % (pretty_k, v.id)) + else: + if isinstance(v, Gitlab): + continue + v = GitlabObject._obj_to_str(v) + print("%s%s: %s" % (" " * depth * 2, pretty_k, v)) + + def json(self): + return json.dumps(self.__dict__, cls=jsonEncoder) + + +class UserKey(GitlabObject): + _url = '/users/%(user_id)s/keys' + canGet = False + canUpdate = False + requiredUrlAttrs = ['user_id'] + requiredCreateAttrs = ['title', 'key'] + + +class User(GitlabObject): + _url = '/users' + shortPrintAttr = 'username' + # FIXME: password is required for create but not for update + requiredCreateAttrs = ['email', 'username', 'name'] + optionalCreateAttrs = ['password', 'skype', 'linkedin', 'twitter', + 'projects_limit', 'extern_uid', 'provider', + 'bio', 'admin', 'can_create_group', 'website_url'] + + def Key(self, id=None, **kwargs): + return UserKey._getListOrObject(self.gitlab, id, + user_id=self.id, + **kwargs) + + +class CurrentUserKey(GitlabObject): + _url = '/user/keys' + canUpdate = False + shortPrintAttr = 'title' + requiredCreateAttrs = ['title', 'key'] + + +class CurrentUser(GitlabObject): + _url = '/user' + canList = False + canCreate = False + canUpdate = False + canDelete = False + shortPrintAttr = 'username' + + def Key(self, id=None, **kwargs): + return CurrentUserKey._getListOrObject(self.gitlab, id, **kwargs) + + +class GroupMember(GitlabObject): + _url = '/groups/%(group_id)s/members' + canGet = False + canUpdate = False + requiredUrlAttrs = ['group_id'] + requiredCreateAttrs = ['access_level', 'user_id'] + shortPrintAttr = 'username' + + +class Group(GitlabObject): + _url = '/groups' + canUpdate = False + _constructorTypes = {'projects': 'Project'} + requiredCreateAttrs = ['name', 'path'] + shortPrintAttr = 'name' + + GUEST_ACCESS = 10 + REPORTER_ACCESS = 20 + DEVELOPER_ACCESS = 30 + MASTER_ACCESS = 40 + OWNER_ACCESS = 50 + + def Member(self, id=None, **kwargs): + return GroupMember._getListOrObject(self.gitlab, id, + group_id=self.id, + **kwargs) + + def transfer_project(self, id, **kwargs): + url = '/groups/%d/projects/%d' % (self.id, id) + r = self.gitlab.rawPost(url, None, **kwargs) + if r.status_code != 201: + _raiseErrorFromResponse(r, GitlabTransferProjectError) + + +class Hook(GitlabObject): + _url = '/hooks' + canUpdate = False + requiredCreateAttrs = ['url'] + shortPrintAttr = 'url' + + +class Issue(GitlabObject): + _url = '/issues' + _constructorTypes = {'author': 'User', 'assignee': 'User', + 'milestone': 'ProjectMilestone'} + canGet = False + canDelete = False + canUpdate = False + canCreate = False + shortPrintAttr = 'title' + + +class ProjectBranch(GitlabObject): + _url = '/projects/%(project_id)s/repository/branches' + _constructorTypes = {'author': 'User', "committer": "User"} + + idAttr = 'name' + canUpdate = False + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['branch_name', 'ref'] + _constructorTypes = {'commit': 'ProjectCommit'} + + def protect(self, protect=True, **kwargs): + 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.rawPut(url, data=None, content_type=None, **kwargs) + + if r.status_code == 200: + if protect: + self.protected = protect + else: + del self.protected + else: + _raiseErrorFromResponse(r, GitlabProtectError) + + def unprotect(self, **kwargs): + self.protect(False, **kwargs) + + +class ProjectCommit(GitlabObject): + _url = '/projects/%(project_id)s/repository/commits' + canDelete = False + canUpdate = False + canCreate = False + requiredUrlAttrs = ['project_id'] + shortPrintAttr = 'title' + + def diff(self, **kwargs): + url = ('/projects/%(project_id)s/repository/commits/%(commit_id)s/diff' + % {'project_id': self.project_id, 'commit_id': self.id}) + r = self.gitlab.rawGet(url, **kwargs) + if r.status_code == 200: + return r.json() + else: + _raiseErrorFromResponse(r, GitlabGetError) + + def blob(self, filepath, **kwargs): + 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.rawGet(url, **kwargs) + if r.status_code == 200: + return r.content + else: + _raiseErrorFromResponse(r, GitlabGetError) + + +class ProjectKey(GitlabObject): + _url = '/projects/%(project_id)s/keys' + canUpdate = False + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['title', 'key'] + + +class ProjectEvent(GitlabObject): + _url = '/projects/%(project_id)s/events' + canGet = False + canDelete = False + canUpdate = False + canCreate = False + requiredUrlAttrs = ['project_id'] + shortPrintAttr = 'target_title' + + +class ProjectHook(GitlabObject): + _url = '/projects/%(project_id)s/hooks' + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['url'] + optionalCreateAttrs = ['push_events', 'issues_events', + 'merge_requests_events', 'tag_push_events'] + shortPrintAttr = 'url' + + +class ProjectIssueNote(GitlabObject): + _url = '/projects/%(project_id)s/issues/%(issue_id)s/notes' + _constructorTypes = {'author': 'User'} + canUpdate = False + canDelete = False + requiredUrlAttrs = ['project_id', 'issue_id'] + requiredCreateAttrs = ['body'] + + +class ProjectIssue(GitlabObject): + _url = '/projects/%(project_id)s/issues/' + _constructorTypes = {'author': 'User', 'assignee': 'User', + 'milestone': 'ProjectMilestone'} + canDelete = False + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['title'] + # FIXME: state_event is only valid with update + optionalCreateAttrs = ['description', 'assignee_id', 'milestone_id', + 'labels', 'state_event'] + + shortPrintAttr = 'title' + + def _dataForGitlab(self, extra_parameters={}): + # Gitlab-api returns labels in a json list and takes them in a + # comma separated list. + if hasattr(self, "labels"): + if (self.labels is not None and + not isinstance(self.labels, six.string_types)): + labels = ", ".join(self.labels) + extra_parameters['labels'] = labels + + return super(ProjectIssue, self)._dataForGitlab(extra_parameters) + + def Note(self, id=None, **kwargs): + return ProjectIssueNote._getListOrObject(self.gitlab, id, + project_id=self.project_id, + issue_id=self.id, + **kwargs) + + +class ProjectMember(GitlabObject): + _url = '/projects/%(project_id)s/members' + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['access_level', 'user_id'] + shortPrintAttr = 'username' + + +class ProjectNote(GitlabObject): + _url = '/projects/%(project_id)s/notes' + _constructorTypes = {'author': 'User'} + canUpdate = False + canDelete = False + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['body'] + + +class ProjectTag(GitlabObject): + _url = '/projects/%(project_id)s/repository/tags' + idAttr = 'name' + canGet = False + canDelete = False + canUpdate = False + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['tag_name', 'ref'] + optionalCreateAttrs = ['message'] + shortPrintAttr = 'name' + + +class ProjectMergeRequestNote(GitlabObject): + _url = '/projects/%(project_id)s/merge_requests/%(merge_request_id)s/notes' + _constructorTypes = {'author': 'User'} + canUpdate = False + canDelete = False + requiredUrlAttrs = ['project_id', 'merge_request_id'] + requiredCreateAttrs = ['body'] + + +class ProjectMergeRequest(GitlabObject): + _url = '/projects/%(project_id)s/merge_requests' + _constructorTypes = {'author': 'User', 'assignee': 'User'} + canDelete = False + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['source_branch', 'target_branch', 'title'] + optionalCreateAttrs = ['assignee_id'] + + def Note(self, id=None, **kwargs): + return ProjectMergeRequestNote._getListOrObject( + self.gitlab, id, project_id=self.project_id, + merge_request_id=self.id, **kwargs) + + +class ProjectMilestone(GitlabObject): + _url = '/projects/%(project_id)s/milestones' + canDelete = False + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['title'] + optionalCreateAttrs = ['description', 'due_date', 'state_event'] + shortPrintAttr = 'title' + + +class ProjectLabel(GitlabObject): + _url = '/projects/%(project_id)s/labels' + requiredUrlAttrs = ['project_id'] + idAttr = 'name' + requiredDeleteAttrs = ['name'] + requiredCreateAttrs = ['name', 'color'] + # FIXME: new_name is only valid with update + optionalCreateAttrs = ['new_name'] + + +class ProjectFile(GitlabObject): + _url = '/projects/%(project_id)s/repository/files' + canList = False + requiredUrlAttrs = ['project_id'] + requiredGetAttrs = ['file_path', 'ref'] + requiredCreateAttrs = ['file_path', 'branch_name', 'content', + 'commit_message'] + optionalCreateAttrs = ['encoding'] + requiredDeleteAttrs = ['branch_name', 'commit_message'] + getListWhenNoId = False + shortPrintAttr = 'name' + + +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 ProjectSnippet(GitlabObject): + _url = '/projects/%(project_id)s/snippets' + _constructorTypes = {'author': 'User'} + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['title', 'file_name', 'code'] + optionalCreateAttrs = ['lifetime'] + shortPrintAttr = 'title' + + def Content(self, **kwargs): + url = "/projects/%(project_id)s/snippets/%(snippet_id)s/raw" % \ + {'project_id': self.project_id, 'snippet_id': self.id} + r = self.gitlab.rawGet(url, **kwargs) + + if r.status_code == 200: + return r.content + else: + _raiseErrorFromResponse(r, GitlabGetError) + + def Note(self, id=None, **kwargs): + return ProjectSnippetNote._getListOrObject(self.gitlab, id, + project_id=self.project_id, + snippet_id=self.id, + **kwargs) + + +class UserProject(GitlabObject): + _url = '/projects/user/%(user_id)s' + _constructorTypes = {'owner': 'User', 'namespace': 'Group'} + canUpdate = False + canDelete = False + canList = False + canGet = False + requiredUrlAttrs = ['user_id'] + requiredCreateAttrs = ['name'] + optionalCreateAttrs = ['default_branch', 'issues_enabled', 'wall_enabled', + 'merge_requests_enabled', 'wiki_enabled', + 'snippets_enabled', 'public', 'visibility_level', + 'description'] + + +class Project(GitlabObject): + _url = '/projects' + _constructorTypes = {'owner': 'User', 'namespace': 'Group'} + canUpdate = False + requiredCreateAttrs = ['name'] + optionalCreateAttrs = ['default_branch', 'issues_enabled', 'wall_enabled', + 'merge_requests_enabled', 'wiki_enabled', + 'snippets_enabled', 'public', 'visibility_level', + 'namespace_id', 'description', 'path', 'import_url'] + + shortPrintAttr = 'path' + + def Branch(self, id=None, **kwargs): + return ProjectBranch._getListOrObject(self.gitlab, id, + project_id=self.id, + **kwargs) + + def Commit(self, id=None, **kwargs): + return ProjectCommit._getListOrObject(self.gitlab, id, + project_id=self.id, + **kwargs) + + def Event(self, id=None, **kwargs): + return ProjectEvent._getListOrObject(self.gitlab, id, + project_id=self.id, + **kwargs) + + def Hook(self, id=None, **kwargs): + return ProjectHook._getListOrObject(self.gitlab, id, + project_id=self.id, + **kwargs) + + def Key(self, id=None, **kwargs): + return ProjectKey._getListOrObject(self.gitlab, id, + project_id=self.id, + **kwargs) + + def Issue(self, id=None, **kwargs): + return ProjectIssue._getListOrObject(self.gitlab, id, + project_id=self.id, + **kwargs) + + def Member(self, id=None, **kwargs): + return ProjectMember._getListOrObject(self.gitlab, id, + project_id=self.id, + **kwargs) + + def MergeRequest(self, id=None, **kwargs): + return ProjectMergeRequest._getListOrObject(self.gitlab, id, + project_id=self.id, + **kwargs) + + def Milestone(self, id=None, **kwargs): + return ProjectMilestone._getListOrObject(self.gitlab, id, + project_id=self.id, + **kwargs) + + def Note(self, id=None, **kwargs): + return ProjectNote._getListOrObject(self.gitlab, id, + project_id=self.id, + **kwargs) + + def Snippet(self, id=None, **kwargs): + return ProjectSnippet._getListOrObject(self.gitlab, id, + project_id=self.id, + **kwargs) + + def Label(self, id=None, **kwargs): + return ProjectLabel._getListOrObject(self.gitlab, id, + project_id=self.id, + **kwargs) + + def File(self, id=None, **kwargs): + return ProjectFile._getListOrObject(self.gitlab, id, + project_id=self.id, + **kwargs) + + def Tag(self, id=None, **kwargs): + return ProjectTag._getListOrObject(self.gitlab, id, + project_id=self.id, + **kwargs) + + def tree(self, path='', ref_name='', **kwargs): + url = "%s/%s/repository/tree" % (self._url, self.id) + url += '?path=%s&ref_name=%s' % (path, ref_name) + r = self.gitlab.rawGet(url, **kwargs) + if r.status_code == 200: + return r.json() + else: + _raiseErrorFromResponse(r, GitlabGetError) + + def blob(self, sha, filepath, **kwargs): + url = "%s/%s/repository/blobs/%s" % (self._url, self.id, sha) + url += '?filepath=%s' % (filepath) + r = self.gitlab.rawGet(url, **kwargs) + if r.status_code == 200: + return r.content + else: + _raiseErrorFromResponse(r, GitlabGetError) + + def archive(self, sha=None, **kwargs): + url = '/projects/%s/repository/archive' % self.id + if sha: + url += '?sha=%s' % sha + r = self.gitlab.rawGet(url, **kwargs) + if r.status_code == 200: + return r.content + else: + _raiseErrorFromResponse(r, GitlabGetError) + + def create_file(self, path, branch, content, message, **kwargs): + """Creates file in project repository + + Args: + path (str): Full path to new file + branch (str): The name of branch + content (str): Content of the file + message (str): Commit message + kwargs: Arbitrary keyword arguments + + Raises: + GitlabCreateError: Operation failed + GitlabConnectionError: Connection to GitLab-server failed + """ + url = "/projects/%s/repository/files" % self.id + url += "?file_path=%s&branch_name=%s&content=%s&commit_message=%s" % \ + (path, branch, content, message) + r = self.gitlab.rawPost(url, data=None, content_type=None, **kwargs) + if r.status_code != 201: + _raiseErrorFromResponse(r, GitlabCreateError) + + def update_file(self, path, branch, content, message, **kwargs): + url = "/projects/%s/repository/files" % self.id + url += "?file_path=%s&branch_name=%s&content=%s&commit_message=%s" % \ + (path, branch, content, message) + r = self.gitlab.rawPut(url, data=None, content_type=None, **kwargs) + if r.status_code != 200: + _raiseErrorFromResponse(r, GitlabUpdateError) + + def delete_file(self, path, branch, message, **kwargs): + url = "/projects/%s/repository/files" % self.id + url += "?file_path=%s&branch_name=%s&commit_message=%s" % \ + (path, branch, message) + r = self.gitlab.rawDelete(url, **kwargs) + if r.status_code != 200: + _raiseErrorFromResponse(r, GitlabDeleteError) + + +class TeamMember(GitlabObject): + _url = '/user_teams/%(team_id)s/members' + canUpdate = False + requiredUrlAttrs = ['teamd_id'] + requiredCreateAttrs = ['access_level'] + shortPrintAttr = 'username' + + +class TeamProject(GitlabObject): + _url = '/user_teams/%(team_id)s/projects' + _constructorTypes = {'owner': 'User', 'namespace': 'Group'} + canUpdate = False + requiredCreateAttrs = ['greatest_access_level'] + requiredUrlAttrs = ['team_id'] + shortPrintAttr = 'name' + + +class Team(GitlabObject): + _url = '/user_teams' + shortPrintAttr = 'name' + requiredCreateAttrs = ['name', 'path'] + canUpdate = False + + def Member(self, id=None, **kwargs): + return TeamMember._getListOrObject(self.gitlab, id, + team_id=self.id, + **kwargs) + + def Project(self, id=None, **kwargs): + return TeamProject._getListOrObject(self.gitlab, id, + team_id=self.id, + **kwargs) |
