From 89d3fa03ae859c85bfbdb6db3c913824b274cecb Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Fri, 5 Sep 2014 23:55:41 -0600 Subject: [PATCH 01/23] refactor "_sanitize" for Python < 2.7 --- gitlab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitlab.py b/gitlab.py index 9ac179cea..50f429e8e 100644 --- a/gitlab.py +++ b/gitlab.py @@ -468,7 +468,7 @@ def _sanitize(value): def _sanitize_dict(src): - return {k: _sanitize(v) for k, v in src.items()} + return dict((k, _sanitize(v)) for k, v in src.items()) class GitlabObject(object): From b48331928225347aeee83af1bc3c1dee64205f9b Mon Sep 17 00:00:00 2001 From: "Amar Sood (tekacs)" Date: Sun, 5 Oct 2014 11:29:21 +0100 Subject: [PATCH 02/23] Update .sort to use key for Python 3.x. Rather than really dubious cmp function. --- gitlab | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/gitlab b/gitlab index 18885006e..7ddd48c54 100755 --- a/gitlab +++ b/gitlab @@ -131,13 +131,7 @@ def usage(): if gitlab.GitlabObject in getmro(o) and o != gitlab.GitlabObject: classes.append(o) - def s(a, b): - if a.__name__ < b.__name__: - return -1 - elif a.__name__ > b.__name__: - return 1 - - classes.sort(cmp=s) + classes.sort(key=lambda x: x.__name__) for cls in classes: print(" %s" % clsToWhat(cls)) From 3cf35cedfff4784af9e7b882b85f71b22ec93c25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20M=C3=A4enp=C3=A4=C3=A4?= Date: Thu, 9 Oct 2014 10:14:12 +0300 Subject: [PATCH 03/23] _setFromDict thinks False is None --- gitlab.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gitlab.py b/gitlab.py index 50f429e8e..342dc0121 100644 --- a/gitlab.py +++ b/gitlab.py @@ -525,10 +525,10 @@ def _setFromDict(self, data): self.__dict__[k] = [] for i in v: self.__dict__[k].append(self._getObject(k, i)) - elif v: - self.__dict__[k] = self._getObject(k, v) - else: # None object + elif v is None: self.__dict__[k] = None + else: + self.__dict__[k] = self._getObject(k, v) def _create(self): if not self.canCreate: From 40ce81e9b9cea0dd75c712ccac887afd37416996 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20M=C3=A4enp=C3=A4=C3=A4?= Date: Thu, 9 Oct 2014 10:17:21 +0300 Subject: [PATCH 04/23] No reason to add kwargs to object in Gitlab.list()-method because GitlabObject constructor can handle them. --- gitlab.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/gitlab.py b/gitlab.py index 50f429e8e..5a3b034ef 100644 --- a/gitlab.py +++ b/gitlab.py @@ -204,14 +204,14 @@ def list(self, obj_class, **kwargs): cls = obj_class if obj_class._returnClass: cls = obj_class._returnClass - l = [cls(self, item) for item in r.json() if item is not None] - if kwargs: - for k, v in kwargs.items(): - if k in ('page', 'per_page'): - continue - for obj in l: - obj.__dict__[k] = str(v) - return l + + # Remove parameters from kwargs before passing it to constructor + cls_kwargs = kwargs.copy() + for key in ['page', 'per_page']: + 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] elif r.status_code == 401: raise GitlabAuthenticationError(r.json()['message']) else: From afcf1c23c36a7aa0f65392892ca4abb973e35b43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20M=C3=A4enp=C3=A4=C3=A4?= Date: Thu, 9 Oct 2014 10:30:41 +0300 Subject: [PATCH 05/23] CurrentUser.Key uses _getListOrObject-method --- gitlab.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/gitlab.py b/gitlab.py index 50f429e8e..0214cfb61 100644 --- a/gitlab.py +++ b/gitlab.py @@ -667,11 +667,7 @@ class CurrentUser(GitlabObject): shortPrintAttr = 'username' def Key(self, id=None, **kwargs): - if id is None: - return CurrentUserKey.list(self.gitlab, **kwargs) - else: - return CurrentUserKey(self.gitlab, id) - + return self._getListOrObject(CurrentUserKey, id, **kwargs) class GroupMember(GitlabObject): _url = '/groups/%(group_id)s/members' From 15c0da5552aa57340d25946bb41d0cd079ec495d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20M=C3=A4enp=C3=A4=C3=A4?= Date: Thu, 9 Oct 2014 11:47:39 +0300 Subject: [PATCH 06/23] Python3 compatibility --- gitlab.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/gitlab.py b/gitlab.py index 268fe5ce1..50eae9639 100644 --- a/gitlab.py +++ b/gitlab.py @@ -15,10 +15,21 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . +from __future__ import print_function, division, absolute_import + +from itertools import chain + import json import requests import sys +if sys.version_info[0] < 3: + PY2=True + str_types = (str, unicode,) +else: + PY2=False + str_types = (str,) + __title__ = 'python-gitlab' __version__ = '0.7' __author__ = 'Gauvain Pocentek' @@ -312,7 +323,7 @@ def update(self, obj): d[k] = str(v) elif type(v) == bool: d[k] = 1 if v else 0 - elif type(v) == unicode: + elif PY2 and type(v) == unicode: d[k] = str(v.encode(self.gitlab_encoding, "replace")) try: @@ -462,7 +473,7 @@ def _get_display_encoding(): def _sanitize(value): - if type(value) in (str, unicode): + if type(value) in str_types: return value.replace('/', '%2F') return value @@ -562,7 +573,7 @@ def delete(self): def __init__(self, gl, data=None, **kwargs): self.gitlab = gl - if data is None or type(data) in [int, str, unicode]: + if data is None or type(data) in chain((int,), str_types): data = self.gitlab.get(self.__class__, data, **kwargs) self._setFromDict(data) @@ -598,7 +609,7 @@ def _obj_to_str(obj): elif isinstance(obj, list): s = ", ".join([GitlabObject._obj_to_str(x) for x in obj]) return "[ %s ]" % s - elif isinstance(obj, unicode): + elif PY2 and isinstance(obj, unicode): return obj.encode(_get_display_encoding(), "replace") else: return str(obj) @@ -611,7 +622,8 @@ def pretty_print(self, depth=0): continue v = self.__dict__[k] pretty_k = k.replace('_', '-') - pretty_k = pretty_k.encode(_get_display_encoding(), "replace") + if PY2: + pretty_k = pretty_k.encode(_get_display_encoding(), "replace") if isinstance(v, GitlabObject): if depth == 0: print("%s:" % pretty_k) From d2e591ec75aec916f3b37192ddcdc2163d558995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20M=C3=A4enp=C3=A4=C3=A4?= Date: Thu, 9 Oct 2014 11:05:45 +0300 Subject: [PATCH 07/23] Timeout support --- gitlab.py | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/gitlab.py b/gitlab.py index 268fe5ce1..93cc2dfdc 100644 --- a/gitlab.py +++ b/gitlab.py @@ -75,15 +75,19 @@ class GitlabAuthenticationError(Exception): class Gitlab(object): """Represents a GitLab server connection""" def __init__(self, url, private_token=None, - email=None, password=None, ssl_verify=True): + email=None, password=None, ssl_verify=True, timeout=None): """Stores informations about the server url: the URL of the Gitlab server private_token: the user private token email: the user email/login password: the user password (associated with email) + ssl_verify: (Passed to requests-library) + timeout: (Passed to requests-library). Timeout to use for requests to + gitlab server. Float or tuple(Float,Float). """ self._url = '%s/api/v3' % url + self.timeout = timeout self.setToken(private_token) self.email = email self.password = password @@ -141,7 +145,8 @@ def rawGet(self, path, **kwargs): try: return requests.get(url, headers=self.headers, - verify=self.ssl_verify) + verify=self.ssl_verify, + timeout=self.timeout) except: raise GitlabConnectionError( "Can't connect to GitLab server (%s)" % self._url) @@ -151,7 +156,8 @@ def rawPost(self, path, data=None): try: return requests.post(url, data, headers=self.headers, - verify=self.ssl_verify) + verify=self.ssl_verify, + timeout=self.timeout) except: raise GitlabConnectionError( "Can't connect to GitLab server (%s)" % self._url) @@ -162,7 +168,8 @@ def rawPut(self, path): try: return requests.put(url, headers=self.headers, - verify=self.ssl_verify) + verify=self.ssl_verify, + timeout=self.timeout) except: raise GitlabConnectionError( "Can't connect to GitLab server (%s)" % self._url) @@ -173,7 +180,8 @@ def rawDelete(self, path): try: return requests.delete(url, headers=self.headers, - verify=self.ssl_verify) + verify=self.ssl_verify, + timeout=self.timeout) except: raise GitlabConnectionError( "Can't connect to GitLab server (%s)" % self._url) @@ -195,7 +203,8 @@ def list(self, obj_class, **kwargs): ["%s=%s" % (k, v) for k, v in args.items()])) try: - r = requests.get(url, headers=self.headers, verify=self.ssl_verify) + r = requests.get(url, headers=self.headers, verify=self.ssl_verify, + timeout=self.timeout) except: raise GitlabConnectionError( "Can't connect to GitLab server (%s)" % self._url) @@ -233,7 +242,8 @@ def get(self, obj_class, id=None, **kwargs): url = '%s%s' % (self._url, url) try: - r = requests.get(url, headers=self.headers, verify=self.ssl_verify) + r = requests.get(url, headers=self.headers, verify=self.ssl_verify, + timeout=self.timeout) except: raise GitlabConnectionError( "Can't connect to GitLab server (%s)" % self._url) @@ -255,7 +265,8 @@ def delete(self, obj): try: r = requests.delete(url, headers=self.headers, - verify=self.ssl_verify) + verify=self.ssl_verify, + timeout=self.timeout) except: raise GitlabConnectionError( "Can't connect to GitLab server (%s)" % self._url) @@ -288,7 +299,8 @@ def create(self, obj): try: r = requests.post(url, obj.__dict__, headers=self.headers, - verify=self.ssl_verify) + verify=self.ssl_verify, + timeout=self.timeout) except: raise GitlabConnectionError( "Can't connect to GitLab server (%s)" % self._url) @@ -318,7 +330,8 @@ def update(self, obj): try: r = requests.put(url, d, headers=self.headers, - verify=self.ssl_verify) + verify=self.ssl_verify, + timeout=self.timeout) except: raise GitlabConnectionError( "Can't connect to GitLab server (%s)" % self._url) From d714c4d35bc627d9113a4925f843c54d6123e621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20M=C3=A4enp=C3=A4=C3=A4?= Date: Mon, 13 Oct 2014 09:59:33 +0300 Subject: [PATCH 08/23] Python 3 compatibility for cli-program --- gitlab | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gitlab b/gitlab index 7ddd48c54..f0aa46069 100755 --- a/gitlab +++ b/gitlab @@ -16,6 +16,8 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . +from __future__ import print_function, division, absolute_import + import os import sys import re From 431e4bdf089354534f6877d39631ff23038e8866 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20M=C3=A4enp=C3=A4=C3=A4?= Date: Mon, 13 Oct 2014 16:40:09 +0300 Subject: [PATCH 09/23] Py3 compatibility with six --- gitlab.py | 24 +++++++++--------------- requirements.txt | 1 + setup.py | 2 +- 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/gitlab.py b/gitlab.py index 50eae9639..60740020c 100644 --- a/gitlab.py +++ b/gitlab.py @@ -17,19 +17,12 @@ from __future__ import print_function, division, absolute_import -from itertools import chain +import six import json import requests import sys -if sys.version_info[0] < 3: - PY2=True - str_types = (str, unicode,) -else: - PY2=False - str_types = (str,) - __title__ = 'python-gitlab' __version__ = '0.7' __author__ = 'Gauvain Pocentek' @@ -292,7 +285,7 @@ def create(self, obj): url = obj._url % args url = '%s%s' % (self._url, url) - for k, v in obj.__dict__.items(): + for k, v in list(obj.__dict__.items()): if type(v) == bool: obj.__dict__[k] = 1 if v else 0 @@ -318,12 +311,12 @@ def update(self, obj): # build a dict of data that can really be sent to server d = {} - for k, v in obj.__dict__.items(): + for k, v in list(obj.__dict__.items()): if type(v) in (int, str): d[k] = str(v) elif type(v) == bool: d[k] = 1 if v else 0 - elif PY2 and type(v) == unicode: + elif six.PY2 and type(v) == six.text_type: d[k] = str(v.encode(self.gitlab_encoding, "replace")) try: @@ -473,7 +466,7 @@ def _get_display_encoding(): def _sanitize(value): - if type(value) in str_types: + if isinstance(value, six.string_types): return value.replace('/', '%2F') return value @@ -573,7 +566,8 @@ def delete(self): def __init__(self, gl, data=None, **kwargs): self.gitlab = gl - if data is None or type(data) in chain((int,), str_types): + if data is None or isinstance(data, six.integer_types) or\ + isinstance(data, six.string_types): data = self.gitlab.get(self.__class__, data, **kwargs) self._setFromDict(data) @@ -609,7 +603,7 @@ def _obj_to_str(obj): elif isinstance(obj, list): s = ", ".join([GitlabObject._obj_to_str(x) for x in obj]) return "[ %s ]" % s - elif PY2 and isinstance(obj, unicode): + elif six.PY2 and isinstance(obj, six.text_type): return obj.encode(_get_display_encoding(), "replace") else: return str(obj) @@ -622,7 +616,7 @@ def pretty_print(self, depth=0): continue v = self.__dict__[k] pretty_k = k.replace('_', '-') - if PY2: + if six.PY2: pretty_k = pretty_k.encode(_get_display_encoding(), "replace") if isinstance(v, GitlabObject): if depth == 0: diff --git a/requirements.txt b/requirements.txt index 65d5e04b2..af8843719 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ requests>1.0 +six diff --git a/setup.py b/setup.py index eeeeaf0d1..4330e6c2a 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def get_version(): url='https://github.com/gpocentek/python-gitlab', py_modules=['gitlab'], scripts=['gitlab'], - install_requires=['requests'], + install_requires=['requests', 'six'], classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Console', From ee54b3e6927f6c8d3b5f9bcbec0e67b94be8566d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20M=C3=A4enp=C3=A4=C3=A4?= Date: Tue, 14 Oct 2014 14:14:08 +0300 Subject: [PATCH 10/23] Gitlab.get() raised GitlabListError instead of GitlabGetError --- gitlab.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gitlab.py b/gitlab.py index 93cc2dfdc..c6cb6d7a4 100644 --- a/gitlab.py +++ b/gitlab.py @@ -232,8 +232,8 @@ def get(self, obj_class, id=None, **kwargs): if k not in kwargs: missing.append(k) if missing: - raise GitlabListError('Missing attribute(s): %s' % - ", ".join(missing)) + raise GitlabGetError('Missing attribute(s): %s' % + ", ".join(missing)) url = obj_class._url % _sanitize_dict(kwargs) if id is not None: From e14e3bf0f675c54930af53c832ccd7ab98df89f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20M=C3=A4enp=C3=A4=C3=A4?= Date: Thu, 9 Oct 2014 12:01:42 +0300 Subject: [PATCH 11/23] Moved url-construction to separate function --- gitlab.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/gitlab.py b/gitlab.py index 93cc2dfdc..1f930addb 100644 --- a/gitlab.py +++ b/gitlab.py @@ -126,6 +126,15 @@ 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 setToken(self, token): """Sets the private token for authentication""" self.private_token = token if token else None @@ -195,9 +204,8 @@ def list(self, obj_class, **kwargs): raise GitlabListError('Missing attribute(s): %s' % ", ".join(missing)) + url = self.constructUrl(id_=None, obj=obj_class, parameters=kwargs) args = _sanitize_dict(kwargs) - url = obj_class._url % args - url = '%s%s' % (self._url, url) if args: url += "?%s" % ("&".join( ["%s=%s" % (k, v) for k, v in args.items()])) @@ -235,11 +243,7 @@ def get(self, obj_class, id=None, **kwargs): raise GitlabListError('Missing attribute(s): %s' % ", ".join(missing)) - url = obj_class._url % _sanitize_dict(kwargs) - if id is not None: - url = '%s%s/%s' % (self._url, url, str(id)) - else: - url = '%s%s' % (self._url, url) + url = self.constructUrl(id_=id, obj=obj_class, parameters=kwargs) try: r = requests.get(url, headers=self.headers, verify=self.ssl_verify, @@ -258,9 +262,7 @@ def get(self, obj_class, id=None, **kwargs): raise GitlabGetError('%d: %s' % (r.status_code, r.text)) def delete(self, obj): - args = _sanitize_dict(obj.__dict__) - url = obj._url % args - url = '%s%s/%s' % (self._url, url, args['id']) + url = self.constructUrl(id_=obj.id, obj=obj, parameters=obj.__dict__) try: r = requests.delete(url, @@ -288,9 +290,7 @@ def create(self, obj): raise GitlabCreateError('Missing attribute(s): %s' % ", ".join(missing)) - args = _sanitize_dict(obj.__dict__) - url = obj._url % args - url = '%s%s' % (self._url, url) + url = self.constructUrl(id_=None, obj=obj, parameters=obj.__dict__) for k, v in obj.__dict__.items(): if type(v) == bool: @@ -313,9 +313,7 @@ def create(self, obj): raise GitlabCreateError('%d: %s' % (r.status_code, r.text)) def update(self, obj): - args = _sanitize_dict(obj.__dict__) - url = obj._url % args - url = '%s%s/%s' % (self._url, url, str(obj.id)) + url = self.constructUrl(id_=obj.id, obj=obj, parameters=obj.__dict__) # build a dict of data that can really be sent to server d = {} From ea4c099532993cdb3ea547fcbd931127c03fdffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20M=C3=A4enp=C3=A4=C3=A4?= Date: Wed, 15 Oct 2014 15:56:28 +0300 Subject: [PATCH 12/23] Moved url attributes to separate list. Added list for delete attributes. --- gitlab.py | 177 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 102 insertions(+), 75 deletions(-) diff --git a/gitlab.py b/gitlab.py index 43ba54397..7acd728e2 100644 --- a/gitlab.py +++ b/gitlab.py @@ -19,6 +19,8 @@ import requests import sys +from itertools import chain + __title__ = 'python-gitlab' __version__ = '0.7' __author__ = 'Gauvain Pocentek' @@ -197,7 +199,8 @@ def rawDelete(self, path): def list(self, obj_class, **kwargs): missing = [] - for k in obj_class.requiredListAttrs: + for k in chain(obj_class.requiredUrlAttrs, + obj_class.requiredListAttrs): if k not in kwargs: missing.append(k) if missing: @@ -205,13 +208,16 @@ def list(self, obj_class, **kwargs): ", ".join(missing)) url = self.constructUrl(id_=None, obj=obj_class, parameters=kwargs) - args = _sanitize_dict(kwargs) - if args: - url += "?%s" % ("&".join( - ["%s=%s" % (k, v) for k, v in args.items()])) + + # 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, headers=self.headers, verify=self.ssl_verify, + r = requests.get(url, params=kwargs, headers=self.headers, + verify=self.ssl_verify, timeout=self.timeout) except: raise GitlabConnectionError( @@ -227,7 +233,7 @@ def list(self, obj_class, **kwargs): for key in ['page', 'per_page']: 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] elif r.status_code == 401: raise GitlabAuthenticationError(r.json()['message']) @@ -236,7 +242,8 @@ def list(self, obj_class, **kwargs): def get(self, obj_class, id=None, **kwargs): missing = [] - for k in obj_class.requiredGetAttrs: + for k in chain(obj_class.requiredUrlAttrs, + obj_class.requiredGetAttrs): if k not in kwargs: missing.append(k) if missing: @@ -245,9 +252,15 @@ def get(self, obj_class, id=None, **kwargs): url = self.constructUrl(id_=id, obj=obj_class, parameters=kwargs) + # 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, headers=self.headers, verify=self.ssl_verify, - timeout=self.timeout) + r = requests.get(url, params=params, headers=self.headers, + verify=self.ssl_verify, timeout=self.timeout) except: raise GitlabConnectionError( "Can't connect to GitLab server (%s)" % self._url) @@ -262,10 +275,25 @@ def get(self, obj_class, id=None, **kwargs): raise GitlabGetError('%d: %s' % (r.status_code, r.text)) def delete(self, obj): - url = self.constructUrl(id_=obj.id, obj=obj, parameters=obj.__dict__) + params = obj.__dict__.copy() + 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) + + # 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=self.headers, verify=self.ssl_verify, timeout=self.timeout) @@ -283,7 +311,7 @@ def delete(self, obj): def create(self, obj): missing = [] - for k in obj.requiredCreateAttrs: + for k in chain(obj.requiredUrlAttrs, obj.requiredCreateAttrs): if k not in obj.__dict__: missing.append(k) if missing: @@ -313,8 +341,14 @@ def create(self, obj): raise GitlabCreateError('%d: %s' % (r.status_code, r.text)) def update(self, obj): + missing = [] + for k in chain(obj.requiredUrlAttrs, obj.requiredCreateAttrs): + if k not in obj.__dict__: + missing.append(k) + if missing: + raise GitlabUpdateError('Missing attribute(s): %s' % + ", ".join(missing)) url = self.constructUrl(id_=obj.id, obj=obj, parameters=obj.__dict__) - # build a dict of data that can really be sent to server d = {} for k, v in obj.__dict__.items(): @@ -491,8 +525,10 @@ class GitlabObject(object): canCreate = True canUpdate = True canDelete = True + requiredUrlAttrs = [] requiredListAttrs = [] requiredGetAttrs = [] + requiredDeleteAttrs = [] requiredCreateAttrs = [] optionalCreateAttrs = [] idAttr = 'id' @@ -642,19 +678,20 @@ def json(self): class UserKey(GitlabObject): _url = '/users/%(user_id)s/keys' canGet = False - canList = True canUpdate = False - canDelete = True - requiredCreateAttrs = ['user_id', 'title', 'key'] + requiredUrlAttrs = ['user_id'] + requiredCreateAttrs = ['title', 'key'] class User(GitlabObject): _url = '/users' shortPrintAttr = 'username' - requiredCreateAttrs = ['email', 'password', 'username', 'name'] - optionalCreateAttrs = ['skype', 'linkedin', 'twitter', 'projects_limit', - 'extern_uid', 'provider', 'bio', 'admin', - 'can_create_group'] + # 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 self._getListOrObject(UserKey, id, @@ -684,13 +721,14 @@ class GroupMember(GitlabObject): _url = '/groups/%(group_id)s/members' canGet = False canUpdate = False - requiredCreateAttrs = ['group_id', 'user_id', 'access_level'] - requiredDeleteAttrs = ['group_id', 'user_id'] + 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' @@ -733,12 +771,12 @@ class Issue(GitlabObject): class ProjectBranch(GitlabObject): _url = '/projects/%(project_id)s/repository/branches' + _constructorTypes = {'author': 'User', "committer": "User"} + idAttr = 'name' canUpdate = False - requiredGetAttrs = ['project_id'] - requiredListAttrs = ['project_id'] - requiredCreateAttrs = ['project_id', 'branch_name', 'ref'] - requiredDeleteAttrs = ['project_id'] + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['branch_name', 'ref'] _constructorTypes = {'commit': 'ProjectCommit'} def protect(self, protect=True): @@ -764,7 +802,7 @@ class ProjectCommit(GitlabObject): canDelete = False canUpdate = False canCreate = False - requiredListAttrs = ['project_id'] + requiredUrlAttrs = ['project_id'] shortPrintAttr = 'title' def diff(self): @@ -790,9 +828,8 @@ def blob(self, filepath): class ProjectKey(GitlabObject): _url = '/projects/%(project_id)s/keys' canUpdate = False - requiredListAttrs = ['project_id'] - requiredGetAttrs = ['project_id'] - requiredCreateAttrs = ['project_id', 'title', 'key'] + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['title', 'key'] class ProjectEvent(GitlabObject): @@ -801,15 +838,16 @@ class ProjectEvent(GitlabObject): canDelete = False canUpdate = False canCreate = False - requiredListAttrs = ['project_id'] + requiredUrlAttrs = ['project_id'] shortPrintAttr = 'target_title' class ProjectHook(GitlabObject): _url = '/projects/%(project_id)s/hooks' - requiredListAttrs = ['project_id'] - requiredGetAttrs = ['project_id'] - requiredCreateAttrs = ['project_id', 'url'] + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['url'] + optionalCreateAttrs = ['push_events', 'issues_events', + 'merge_requests_events', 'tag_push_events'] shortPrintAttr = 'url' @@ -818,9 +856,8 @@ class ProjectIssueNote(GitlabObject): _constructorTypes = {'author': 'User'} canUpdate = False canDelete = False - requiredListAttrs = ['project_id', 'issue_id'] - requiredGetAttrs = ['project_id', 'issue_id'] - requiredCreateAttrs = ['project_id', 'body'] + requiredUrlAttrs = ['project_id', 'issue_id'] + requiredCreateAttrs = ['body'] class ProjectIssue(GitlabObject): @@ -828,11 +865,11 @@ class ProjectIssue(GitlabObject): _constructorTypes = {'author': 'User', 'assignee': 'User', 'milestone': 'ProjectMilestone'} canDelete = False - requiredListAttrs = ['project_id'] - requiredGetAttrs = ['project_id'] - requiredCreateAttrs = ['project_id', 'title'] + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['title'] optionalCreateAttrs = ['description', 'assignee_id', 'milestone_id', 'labels'] + shortPrintAttr = 'title' def Note(self, id=None, **kwargs): @@ -844,9 +881,8 @@ def Note(self, id=None, **kwargs): class ProjectMember(GitlabObject): _url = '/projects/%(project_id)s/members' - requiredListAttrs = ['project_id'] - requiredGetAttrs = ['project_id'] - requiredCreateAttrs = ['project_id', 'user_id', 'access_level'] + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['access_level', 'user_id'] shortPrintAttr = 'username' @@ -855,9 +891,8 @@ class ProjectNote(GitlabObject): _constructorTypes = {'author': 'User'} canUpdate = False canDelete = False - requiredListAttrs = ['project_id'] - requiredGetAttrs = ['project_id'] - requiredCreateAttrs = ['project_id', 'body'] + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['body'] class ProjectTag(GitlabObject): @@ -866,29 +901,27 @@ class ProjectTag(GitlabObject): canGet = False canDelete = False canUpdate = False - canCreate = False - requiredListAttrs = ['project_id'] + 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'} - canGet = False - canCreate = False canUpdate = False canDelete = False - requiredListAttrs = ['project_id', 'merge_request_id'] + 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 - requiredListAttrs = ['project_id'] - requiredGetAttrs = ['project_id'] - requiredCreateAttrs = ['project_id', 'source_branch', - 'target_branch', 'title'] + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['source_branch', 'target_branch', 'title'] optionalCreateAttrs = ['assignee_id'] def Note(self, id=None, **kwargs): @@ -901,9 +934,8 @@ def Note(self, id=None, **kwargs): class ProjectMilestone(GitlabObject): _url = '/projects/%(project_id)s/milestones' canDelete = False - requiredListAttrs = ['project_id'] - requiredGetAttrs = ['project_id'] - requiredCreateAttrs = ['project_id', 'title'] + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['title'] optionalCreateAttrs = ['description', 'due_date', 'state_event'] shortPrintAttr = 'title' @@ -913,17 +945,15 @@ class ProjectSnippetNote(GitlabObject): _constructorTypes = {'author': 'User'} canUpdate = False canDelete = False - requiredListAttrs = ['project_id', 'snippet_id'] - requiredGetAttrs = ['project_id', 'snippet_id'] - requiredCreateAttrs = ['project_id', 'snippet_id', 'body'] + requiredUrlAttrs = ['project_id', 'snippet_id'] + requiredCreateAttrs = ['body'] class ProjectSnippet(GitlabObject): _url = '/projects/%(project_id)s/snippets' _constructorTypes = {'author': 'User'} - requiredListAttrs = ['project_id'] - requiredGetAttrs = ['project_id'] - requiredCreateAttrs = ['project_id', 'title', 'file_name', 'code'] + requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['title', 'file_name', 'code'] optionalCreateAttrs = ['lifetime'] shortPrintAttr = 'title' @@ -951,7 +981,8 @@ class UserProject(GitlabObject): canDelete = False canList = False canGet = False - requiredCreateAttrs = ['name', 'user_id'] + requiredUrlAttrs = ['user_id'] + requiredCreateAttrs = ['name'] optionalCreateAttrs = ['default_branch', 'issues_enabled', 'wall_enabled', 'merge_requests_enabled', 'wiki_enabled', 'snippets_enabled', 'public', 'visibility_level', @@ -962,12 +993,12 @@ class Project(GitlabObject): _url = '/projects' _constructorTypes = {'owner': 'User', 'namespace': 'Group'} canUpdate = False - canDelete = False requiredCreateAttrs = ['name'] optionalCreateAttrs = ['default_branch', 'issues_enabled', 'wall_enabled', 'merge_requests_enabled', 'wiki_enabled', 'snippets_enabled', 'public', 'visibility_level', - 'namespace_id', 'description'] + 'namespace_id', 'description', 'path', 'import_url'] + shortPrintAttr = 'path' def Branch(self, id=None, **kwargs): @@ -1086,10 +1117,8 @@ def delete_file(self, path, branch, message): class TeamMember(GitlabObject): _url = '/user_teams/%(team_id)s/members' canUpdate = False - requiredCreateAttrs = ['team_id', 'user_id', 'access_level'] - requiredDeleteAttrs = ['team_id'] - requiredGetAttrs = ['team_id'] - requiredListAttrs = ['team_id'] + requiredUrlAttrs = ['teamd_id'] + requiredCreateAttrs = ['access_level'] shortPrintAttr = 'username' @@ -1097,10 +1126,8 @@ class TeamProject(GitlabObject): _url = '/user_teams/%(team_id)s/projects' _constructorTypes = {'owner': 'User', 'namespace': 'Group'} canUpdate = False - requiredCreateAttrs = ['team_id', 'project_id', 'greatest_access_level'] - requiredDeleteAttrs = ['team_id', 'project_id'] - requiredGetAttrs = ['team_id'] - requiredListAttrs = ['team_id'] + requiredCreateAttrs = ['greatest_access_level'] + requiredUrlAttrs = ['team_id'] shortPrintAttr = 'name' From c3ab869711276522fe2997ba6e6332704a059d22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20M=C3=A4enp=C3=A4=C3=A4?= Date: Wed, 15 Oct 2014 16:48:56 +0300 Subject: [PATCH 13/23] Support api-objects which don't have id in api response. --- gitlab.py | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/gitlab.py b/gitlab.py index 43ba54397..8b9166cd8 100644 --- a/gitlab.py +++ b/gitlab.py @@ -221,7 +221,11 @@ def list(self, obj_class, **kwargs): cls = obj_class if obj_class._returnClass: cls = obj_class._returnClass - + + # 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 cls_kwargs = kwargs.copy() for key in ['page', 'per_page']: @@ -486,6 +490,8 @@ class GitlabObject(object): _url = None _returnClass = None _constructorTypes = None + # Tells if _getListOrObject should return list or object when id is None + getListWhenNoId = True canGet = True canList = True canCreate = True @@ -509,16 +515,18 @@ def list(cls, gl, **kwargs): return gl.list(cls, **kwargs) def _getListOrObject(self, cls, id, **kwargs): - if id is None: + if id is None and cls.getListWhenNoId: if not cls.canList: - raise GitlabGetError + raise GitlabListError return cls.list(self.gitlab, **kwargs) - + elif id is None and not cls.getListWhenNoId: + if not cls.canGet: + raise GitlabGetError + return cls(self.gitlab, id, **kwargs) elif isinstance(id, dict): if not cls.canCreate: raise GitlabCreateError return cls(self.gitlab, id, **kwargs) - else: if not cls.canGet: raise GitlabGetError @@ -547,6 +555,7 @@ def _create(self): json = self.gitlab.create(self) self._setFromDict(json) + self._created = True def _update(self): if not self.canUpdate: @@ -556,7 +565,7 @@ def _update(self): self._setFromDict(json) def save(self): - if hasattr(self, 'id'): + if self._created: self._update() else: self._create() @@ -565,16 +574,19 @@ def delete(self): if not self.canDelete: raise NotImplementedError - if not hasattr(self, 'id'): + if not self._created: raise GitlabDeleteError return self.gitlab.delete(self) def __init__(self, gl, data=None, **kwargs): + self._created = False self.gitlab = gl if data is None or type(data) in [int, str, unicode]: data = self.gitlab.get(self.__class__, data, **kwargs) + # Object is created because we got it from api + self._created = True self._setFromDict(data) @@ -582,6 +594,12 @@ def __init__(self, gl, data=None, **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__)) From ad63e17ce7b6fd8c8eef993a44a1b18cc73fc4be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20M=C3=A4enp=C3=A4=C3=A4?= Date: Thu, 9 Oct 2014 13:30:09 +0300 Subject: [PATCH 14/23] Classes for ProjectLabels and ProjectFiles --- gitlab.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/gitlab.py b/gitlab.py index 43ba54397..ed9243ee2 100644 --- a/gitlab.py +++ b/gitlab.py @@ -908,6 +908,29 @@ class ProjectMilestone(GitlabObject): shortPrintAttr = 'title' +class ProjectLabel(GitlabObject): + _url = '/projects/%(project_id)s/labels' + requiredUrlAttrs = ['project_id'] + requiredDeleteAttrs = ['name'] + requiredCreateAttrs = ['name', 'color'] + # FIXME: new_name is only valid with update + optionalCreateAttrs = ['new_name'] + shortPrintAttr = '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'} @@ -1025,6 +1048,16 @@ def Snippet(self, id=None, **kwargs): project_id=self.id, **kwargs) + def Label(self, id=None, **kwargs): + return self._getListOrObject(ProjectLabel, id, + project_id=self.id, + **kwargs) + + def File(self, id=None, **kwargs): + return self._getListOrObject(ProjectFile, id, + project_id=self.id, + **kwargs) + def Tag(self, id=None, **kwargs): return self._getListOrObject(ProjectTag, id, project_id=self.id, From 134fc7ac024aa96b1d22cc421a081df6cd2724f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20M=C3=A4enp=C3=A4=C3=A4?= Date: Thu, 16 Oct 2014 09:33:25 +0300 Subject: [PATCH 15/23] Fixed object creation in list --- gitlab.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gitlab.py b/gitlab.py index 8b9166cd8..fc03794a4 100644 --- a/gitlab.py +++ b/gitlab.py @@ -222,12 +222,13 @@ def list(self, obj_class, **kwargs): 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 - cls_kwargs = kwargs.copy() for key in ['page', 'per_page']: if key in cls_kwargs: del cls_kwargs[key] From 526f1be10d06f4359a5b90e485be02632e5c929f Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sat, 25 Oct 2014 08:22:04 +0200 Subject: [PATCH 16/23] =?UTF-8?q?Add=20Mika=20M=C3=A4enp=C3=A4=C3=A4=20in?= =?UTF-8?q?=20the=20authors=20list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AUTHORS | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index b086f42b0..058febbaf 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,7 +1,8 @@ -Author ------- +Authors +------- Gauvain Pocentek +Mika Mäenpää Contributors ------------ From fa9215504d8b6dae2c776733721c718f2a1f2e1a Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sun, 26 Oct 2014 06:12:07 +0100 Subject: [PATCH 17/23] pretty_print: don't display private attributes --- gitlab.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gitlab.py b/gitlab.py index 649157172..e17be6423 100644 --- a/gitlab.py +++ b/gitlab.py @@ -680,6 +680,8 @@ def pretty_print(self, depth=0): for k in sorted(self.__dict__.keys()): if k == self.idAttr: continue + if k[0] == '_': + continue v = self.__dict__[k] pretty_k = k.replace('_', '-') if six.PY2: From f042d2f67c51c0de8d300ef6b1ee36ff5088cdc4 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sun, 26 Oct 2014 06:21:12 +0100 Subject: [PATCH 18/23] ProjectLabel: use name as id attribute --- gitlab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitlab.py b/gitlab.py index e17be6423..59318f865 100644 --- a/gitlab.py +++ b/gitlab.py @@ -970,11 +970,11 @@ class ProjectMilestone(GitlabObject): 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'] - shortPrintAttr = 'name' class ProjectFile(GitlabObject): From 9744475a9d100a1267ebe0be87acce8895cf3c57 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sun, 26 Oct 2014 06:23:08 +0100 Subject: [PATCH 19/23] make sure to not display both id and idAttr --- gitlab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitlab.py b/gitlab.py index 59318f865..0282a699e 100644 --- a/gitlab.py +++ b/gitlab.py @@ -678,7 +678,7 @@ 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: + if k == self.idAttr or k == 'id': continue if k[0] == '_': continue From 167252924823badfa82b5287c940c5925fb05a9e Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sun, 26 Oct 2014 06:43:37 +0100 Subject: [PATCH 20/23] CLI: support a timout option --- AUTHORS | 1 + gitlab | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 058febbaf..6553ec6a7 100644 --- a/AUTHORS +++ b/AUTHORS @@ -14,3 +14,4 @@ Koen Smets Mart Sõmermaa Diego Giovane Pasqualin Crestez Dan Leonard +Patrick Miller diff --git a/gitlab b/gitlab index f0aa46069..433299574 100755 --- a/gitlab +++ b/gitlab @@ -141,7 +141,7 @@ def usage(): def do_auth(): try: gl = gitlab.Gitlab(gitlab_url, private_token=gitlab_token, - ssl_verify=ssl_verify) + ssl_verify=ssl_verify, timeout=timeout) gl.auth() except: die("Could not connect to GitLab (%s)" % gitlab_url) @@ -247,6 +247,7 @@ def do_project_owned(): ssl_verify = True +timeout = 60 gitlab_id = None verbose = False @@ -325,6 +326,15 @@ try: except: pass +try: + timeout = config.getboolean('global', 'timeout') +except: + pass +try: + timeout = config.getboolean(gitlab_id, 'timeout') +except: + pass + try: what = args.pop(0) action = args.pop(0) From 8be5365ef198ddab12df78e9e7bd0ca971e81724 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sun, 26 Oct 2014 06:43:57 +0100 Subject: [PATCH 21/23] Update the Changelog --- ChangeLog | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ChangeLog b/ChangeLog index dc2633a9d..6ed622f06 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,13 @@ +Version 0.8 + + * Better python 2.6 and python 3 support + * Timeout support in HTTP requests + * Gitlab.get() raised GitlabListError instead of GitlabGetError + * Support api-objects which don't have id in api response + * Add ProjectLabel and ProjectFile classes + * Moved url attributes to separate list + * Added list for delete attributes + Version 0.7 * Fix license classifier in setup.py From 39aa99873e121b4e06ec0876b5b0219ac8944195 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sun, 26 Oct 2014 06:46:11 +0100 Subject: [PATCH 22/23] "Document" the timeout option --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index bee94e823..00e701773 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ Here's an example of the syntax: [global] default = local ssl_verify = true +timeout = 5 [local] url = http://10.0.3.2:8080 @@ -93,6 +94,9 @@ authentication is supported (not user/password). The `ssl_verify` option defines if the server SSL certificate should be validated (use false for self signed certificates, only useful with https). +The `timeout` option defines after how many seconds a request to the Gitlab +server should be abandonned. + Choosing a different server than the default one can be done at run time: ````` From 296a72f9f137c2d5ea54e8f72a5fbe9c9833ee85 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Sun, 26 Oct 2014 06:48:04 +0100 Subject: [PATCH 23/23] bump version to 0.8 --- gitlab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitlab.py b/gitlab.py index 0282a699e..3416ebdee 100644 --- a/gitlab.py +++ b/gitlab.py @@ -26,7 +26,7 @@ from itertools import chain __title__ = 'python-gitlab' -__version__ = '0.7' +__version__ = '0.8' __author__ = 'Gauvain Pocentek' __email__ = 'gauvain@pocentek.net' __license__ = 'LGPL3'