diff options
-rw-r--r-- | gitlab/__init__.py | 16 | ||||
-rw-r--r-- | gitlab/base.py | 314 | ||||
-rw-r--r-- | gitlab/v4/objects.py | 175 |
3 files changed, 399 insertions, 106 deletions
diff --git a/gitlab/__init__.py b/gitlab/__init__.py index dbb7f85..d27fcf7 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -644,9 +644,12 @@ class Gitlab(object): opts = self._get_session_opts(content_type='application/json') result = self.session.request(verb, url, json=post_data, params=params, stream=streamed, **opts) - if not (200 <= result.status_code < 300): - raise GitlabHttpError(response_code=result.status_code) - return result + if 200 <= result.status_code < 300: + return result + + + 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. @@ -748,7 +751,7 @@ class Gitlab(object): GitlabHttpError: When the return code is not 2xx GitlabParsingError: IF the json data could not be parsed """ - result = self.hhtp_request('put', path, query_data=query_data, + result = self.http_request('put', path, query_data=query_data, post_data=post_data, **kwargs) try: return result.json() @@ -808,6 +811,9 @@ class GitlabList(object): def __iter__(self): return self + def __len__(self): + return self._total_pages + def __next__(self): return self.next() @@ -819,6 +825,6 @@ class GitlabList(object): except IndexError: if self._next_url: self._query(self._next_url) - return self._data[self._current] + return self.next() raise StopIteration diff --git a/gitlab/base.py b/gitlab/base.py index 0d82cf1..2e26c64 100644 --- a/gitlab/base.py +++ b/gitlab/base.py @@ -531,3 +531,317 @@ class GitlabObject(object): def __ne__(self, other): return not self.__eq__(other) + + +class SaveMixin(object): + """Mixin for RESTObject's that can be updated.""" + def save(self, **kwargs): + """Saves the changes made to the object to the server. + + Args: + **kwargs: Extra option to send to the server (e.g. sudo) + + The object is updated to match what the server returns. + """ + 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) + + # class the manager + obj_id = self.get_id() + server_data = self.manager.update(obj_id, updated_data, **kwargs) + self._updated_attrs = {} + self._attrs.update(server_data) + + +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': {}, + }) + + def __getattr__(self, name): + try: + return self.__dict__['_updated_attrs'][name] + except KeyError: + try: + return self.__dict__['_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 get_id(self): + 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): + 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 GetMixin(object): + def get(self, id, **kwargs): + """Retrieve a single object. + + Args: + id (int or str): ID of the object to retrieve + **kwargs: Extra data to send to the Gitlab server (e.g. sudo) + + Returns: + object: The generated RESTObject. + + Raises: + GitlabGetError: If the server cannot perform the request. + """ + path = '%s/%s' % (self._path, id) + server_data = self.gitlab.http_get(path, **kwargs) + return self._obj_cls(self, server_data) + + +class GetWithoutIdMixin(object): + def get(self, **kwargs): + """Retrieve a single object. + + Args: + **kwargs: Extra data to send to the Gitlab server (e.g. sudo) + + Returns: + object: The generated RESTObject. + + Raises: + 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): + def list(self, **kwargs): + """Retrieves a list of objects. + + Args: + **kwargs: Extra data to send to the Gitlab server (e.g. sudo). + If ``all`` is passed and set to True, the entire list of + objects will be returned. + + Returns: + RESTObjectList: Generator going through the list of objects, making + queries to the server when required. + If ``all=True`` is passed as argument, returns + list(RESTObjectList). + """ + + obj = self.gitlab.http_list(self._path, **kwargs) + if isinstance(obj, list): + return [self._obj_cls(self, item) for item in obj] + else: + return 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 data to send to the Gitlab server (e.g. sudo) + + Returns: + object: The generated RESTObject. + + Raises: + GitlabGetError: If the server cannot perform the request. + """ + gen = self.list() + for obj in gen: + if str(obj.get_id()) == str(id): + return obj + + +class RetrieveMixin(ListMixin, GetMixin): + pass + + +class CreateMixin(object): + def _check_missing_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): + """Returns the required and optional arguments. + + Returns: + tuple: 2 items: list of required arguments and list of optional + arguments for creation (in that order) + """ + if hasattr(self, '_create_attrs'): + return (self._create_attrs['required'], + self._create_attrs['optional']) + return (tuple(), tuple()) + + def create(self, data, **kwargs): + """Created a new object. + + 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) + + Returns: + RESTObject: a new instance of the manage object class build with + the data sent by the server + """ + self._check_missing_attrs(data) + if hasattr(self, '_sanitize_data'): + data = self._sanitize_data(data, 'create') + server_data = self.gitlab.http_post(self._path, post_data=data, **kwargs) + return self._obj_cls(self, server_data) + + +class UpdateMixin(object): + def _check_missing_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): + """Returns the required and optional arguments. + + Returns: + tuple: 2 items: list of required arguments and list of optional + arguments for update (in that order) + """ + if hasattr(self, '_update_attrs'): + return (self._update_attrs['required'], + self._update_attrs['optional']) + return (tuple(), tuple()) + + 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 data to send to the Gitlab server (e.g. sudo) + + Returns: + dict: The new object data (*not* a RESTObject) + """ + + if id is None: + path = self._path + else: + path = '%s/%s' % (self._path, id) + + self._check_missing_attrs(new_data) + if hasattr(self, '_sanitize_data'): + data = self._sanitize_data(new_data, 'update') + server_data = self.gitlab.http_put(self._path, post_data=data, + **kwargs) + return server_data + + +class DeleteMixin(object): + def delete(self, id, **kwargs): + """Deletes an object on the server. + + Args: + id: ID of the object to delete + **kwargs: Extra data to send to the Gitlab server (e.g. sudo) + """ + path = '%s/%s' % (self._path, id) + self.gitlab.http_delete(path, **kwargs) + + +class CRUDMixin(GetMixin, ListMixin, CreateMixin, UpdateMixin, DeleteMixin): + pass + + +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_attrs={}): + self.gitlab = gl + self._parent_attrs = {} # for nested managers diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index 6283149..9e2574e 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -40,7 +40,7 @@ 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 @@ -212,133 +212,106 @@ class CurrentUser(GitlabObject): ) -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 +class ApplicationSettings(SaveMixin, RESTObject): + _id_attr = None + + +class ApplicationSettingsManager(GetWithoutIdMixin, UpdateMixin, RESTManager): + _path = '/application/settings' + _obj_cls = ApplicationSettings + _update_attrs = { + 'required': tuple(), + 'optional': ('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') + } - 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) + 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 ApplicationSettingsManager(BaseManager): - obj_cls = ApplicationSettings +class BroadcastMessage(SaveMixin, RESTObject): + pass -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(CRUDMixin, RESTManager): + _path = '/broadcast_messages' + _obj_cls = BroadcastMessage + _create_attrs = { + 'required': ('message', ), + 'optional': ('starts_at', 'ends_at', 'color', 'font'), + } + _update_attrs = { + 'required': tuple(), + 'optional': ('message', 'starts_at', 'ends_at', 'color', 'font'), + } -class BroadcastMessageManager(BaseManager): - obj_cls = BroadcastMessage +class DeployKey(RESTObject): + pass -class DeployKey(GitlabObject): - _url = '/deploy_keys' - canGet = 'from_list' - canCreate = False - canUpdate = False - canDelete = False +class DeployKeyManager(GetFromListMixin, RESTManager): + _path = '/deploy_keys' + _obj_cls = DeployKey -class DeployKeyManager(BaseManager): - obj_cls = DeployKey +class NotificationSettings(SaveMixin, RESTObject): + _id_attr = None -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(GetWithoutIdMixin, UpdateMixin, RESTManager): + _path = '/notification_settings' + _obj_cls = NotificationSettings -class NotificationSettingsManager(BaseManager): - obj_cls = NotificationSettings + _update_attrs = { + 'required': tuple(), + 'optional': ('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 Dockerfile(GitlabObject): - _url = '/templates/dockerfiles' - canDelete = False - canUpdate = False - canCreate = False - idAttr = 'name' +class Dockerfile(RESTObject): + _id_attr = 'name' -class DockerfileManager(BaseManager): - obj_cls = Dockerfile +class DockerfileManager(RetrieveMixin, RESTManager): + _path = '/templates/dockerfiles' + _obj_cls = Dockerfile -class Gitignore(GitlabObject): - _url = '/templates/gitignores' - canDelete = False - canUpdate = False - canCreate = False - idAttr = 'name' +class Gitignore(RESTObject): + _id_attr = 'name' -class GitignoreManager(BaseManager): - obj_cls = Gitignore +class GitignoreManager(RetrieveMixin, RESTManager): + _path = '/templates/gitignores' + _obj_cls = Gitignore -class Gitlabciyml(GitlabObject): - _url = '/templates/gitlab_ci_ymls' - canDelete = False - canUpdate = False - canCreate = False - idAttr = 'name' +class Gitlabciyml(RESTObject): + _id_attr = 'name' -class GitlabciymlManager(BaseManager): - obj_cls = Gitlabciyml +class GitlabciymlManager(RetrieveMixin, RESTManager): + _path = '/templates/gitlab_ci_ymls' + _obj_cls = Gitlabciyml class GroupIssue(GitlabObject): |