diff options
author | Gauvain Pocentek <gauvain@pocentek.net> | 2017-05-28 22:10:27 +0200 |
---|---|---|
committer | Gauvain Pocentek <gauvain@pocentek.net> | 2017-06-02 15:42:32 +0200 |
commit | f418767ec94c430aabd132d189d1c5e9e2370e68 (patch) | |
tree | a015a30cf17ae755f35a6d1618b2d2a1cccb4472 /gitlab | |
parent | 0467f779eb1d2649f3626e3817531511d3397038 (diff) | |
download | gitlab-f418767ec94c430aabd132d189d1c5e9e2370e68.tar.gz |
Migrate all v4 objects to new API
Some things are probably broken. Next step is writting unit and
functional tests.
And fix.
Diffstat (limited to 'gitlab')
-rw-r--r-- | gitlab/__init__.py | 18 | ||||
-rw-r--r-- | gitlab/base.py | 37 | ||||
-rw-r--r-- | gitlab/exceptions.py | 4 | ||||
-rw-r--r-- | gitlab/mixins.py | 175 | ||||
-rw-r--r-- | gitlab/v4/objects.py | 1853 |
5 files changed, 926 insertions, 1161 deletions
diff --git a/gitlab/__init__.py b/gitlab/__init__.py index 2ea5e14..e9a7e9a 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -683,7 +683,7 @@ class Gitlab(object): raise GitlaParsingError( message="Failed to parse the server message") else: - return r + return result def http_list(self, path, query_data={}, **kwargs): """Make a GET request to the Gitlab server for list-oriented queries. @@ -722,7 +722,8 @@ class Gitlab(object): **kwargs: Extra data to make the query (e.g. sudo, per_page, page) Returns: - The parsed json returned by the server. + The parsed json returned by the server if json is return, else the + raw content. Raises: GitlabHttpError: When the return code is not 2xx @@ -730,11 +731,14 @@ class Gitlab(object): """ result = self.http_request('post', path, query_data=query_data, post_data=post_data, **kwargs) - try: - return result.json() - except Exception: - raise GitlabParsingError( - message="Failed to parse the server message") + if result.headers.get('Content-Type', None) == 'application/json': + try: + return result.json() + except Exception: + raise GitlabParsingError( + message="Failed to parse the server message") + else: + return result.content def http_put(self, path, query_data={}, post_data={}, **kwargs): """Make a PUT request to the Gitlab server. diff --git a/gitlab/base.py b/gitlab/base.py index afbcd38..8949554 100644 --- a/gitlab/base.py +++ b/gitlab/base.py @@ -533,31 +533,6 @@ class GitlabObject(object): 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. @@ -618,6 +593,10 @@ class RESTObject(object): manager = cls(self.manager.gitlab, parent=self) self.__dict__[attr] = manager + def _update_attrs(self, new_attrs): + self._updated_attrs = {} + self._attrs.update(new_attrs) + def get_id(self): if self._id_attr is None: return None @@ -674,13 +653,15 @@ class RESTManager(object): self._parent = parent # for nested managers self._computed_path = self._compute_path() - def _compute_path(self): + def _compute_path(self, path=None): + if path is None: + path = self._path if self._parent is None or not hasattr(self, '_from_parent_attrs'): - return self._path + return path data = {self_attr: getattr(self._parent, parent_attr) for self_attr, parent_attr in self._from_parent_attrs.items()} - return self._path % data + return path % data @property def path(self): diff --git a/gitlab/exceptions.py b/gitlab/exceptions.py index 401e44c..9f27c21 100644 --- a/gitlab/exceptions.py +++ b/gitlab/exceptions.py @@ -39,6 +39,10 @@ class GitlabAuthenticationError(GitlabError): pass +class GitlabParsingError(GitlabError): + pass + + class GitlabConnectionError(GitlabError): pass diff --git a/gitlab/mixins.py b/gitlab/mixins.py index 80ce6c9..0a16a92 100644 --- a/gitlab/mixins.py +++ b/gitlab/mixins.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +import gitlab from gitlab import base @@ -70,7 +71,10 @@ class ListMixin(object): list(RESTObjectList). """ - obj = self.gitlab.http_list(self.path, **kwargs) + # Allow to overwrite the path, handy for custom listings + path = kwargs.pop('path', self.path) + + obj = self.gitlab.http_list(path, **kwargs) if isinstance(obj, list): return [self._obj_cls(self, item) for item in obj] else: @@ -102,7 +106,7 @@ class RetrieveMixin(ListMixin, GetMixin): class CreateMixin(object): - def _check_missing_attrs(self, data): + def _check_missing_create_attrs(self, data): required, optional = self.get_create_attrs() missing = [] for attr in required: @@ -119,13 +123,10 @@ class CreateMixin(object): 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()) + return getattr(self, '_create_attrs', (tuple(), tuple())) def create(self, data, **kwargs): - """Created a new object. + """Creates a new object. Args: data (dict): parameters to send to the server to create the @@ -136,16 +137,17 @@ class CreateMixin(object): RESTObject: a new instance of the manage object class build with the data sent by the server """ - self._check_missing_attrs(data) + self._check_missing_create_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) + # Handle specific URL for creation + path = kwargs.get('path', self.path) + server_data = self.gitlab.http_post(path, post_data=data, **kwargs) return self._obj_cls(self, server_data) class UpdateMixin(object): - def _check_missing_attrs(self, data): + def _check_missing_update_attrs(self, data): required, optional = self.get_update_attrs() missing = [] for attr in required: @@ -162,10 +164,7 @@ class UpdateMixin(object): 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()) + return getattr(self, '_update_attrs', (tuple(), tuple())) def update(self, id=None, new_data={}, **kwargs): """Update an object on the server. @@ -184,9 +183,11 @@ class UpdateMixin(object): else: path = '%s/%s' % (self.path, id) - self._check_missing_attrs(new_data) + self._check_missing_update_attrs(new_data) if hasattr(self, '_sanitize_data'): data = self._sanitize_data(new_data, 'update') + else: + data = new_data server_data = self.gitlab.http_put(path, post_data=data, **kwargs) return server_data @@ -205,3 +206,145 @@ class DeleteMixin(object): class CRUDMixin(GetMixin, ListMixin, CreateMixin, UpdateMixin, DeleteMixin): pass + + +class NoUpdateMixin(GetMixin, ListMixin, CreateMixin, DeleteMixin): + pass + + +class SaveMixin(object): + """Mixin for RESTObject's that can be updated.""" + def _get_updated_data(self): + updated_data = {} + required, optional = self.manager.get_update_attrs() + for attr in required: + # Get everything required, no matter if it's been updated + updated_data[attr] = getattr(self, attr) + # Add the updated attributes + updated_data.update(self._updated_attrs) + + return updated_data + + def save(self, **kwargs): + """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 = self._get_updated_data() + + # call the manager + obj_id = self.get_id() + server_data = self.manager.update(obj_id, updated_data, **kwargs) + self._update_attrs(server_data) + + +class AccessRequestMixin(object): + def approve(self, access_level=gitlab.DEVELOPER_ACCESS, **kwargs): + """Approve an access request. + + Attrs: + access_level (int): The access level for the user. + + Raises: + GitlabConnectionError: If the server cannot be reached. + GitlabUpdateError: If the server fails to perform the request. + """ + + path = '%s/%s/approve' % (self.manager.path, self.id) + data = {'access_level': access_level} + server_data = self.manager.gitlab.http_put(url, post_data=data, + **kwargs) + self._update_attrs(server_data) + + +class SubscribableMixin(object): + def subscribe(self, **kwarg): + """Subscribe to the object notifications. + + raises: + gitlabconnectionerror: if the server cannot be reached. + gitlabsubscribeerror: if the subscription cannot be done + """ + path = '%s/%s/subscribe' % (self.manager.path, self.get_id()) + server_data = self.manager.gitlab.http_post(path, **kwargs) + self._update_attrs(server_data) + + def unsubscribe(self, **kwargs): + """Unsubscribe from the object notifications. + + raises: + gitlabconnectionerror: if the server cannot be reached. + gitlabunsubscribeerror: if the unsubscription cannot be done + """ + path = '%s/%s/unsubscribe' % (self.manager.path, self.get_id()) + server_data = self.manager.gitlab.http_post(path, **kwargs) + self._update_attrs(server_data) + + +class TodoMixin(object): + def todo(self, **kwargs): + """Create a todo associated to the object. + + Raises: + GitlabConnectionError: If the server cannot be reached. + """ + path = '%s/%s/todo' % (self.manager.path, self.get_id()) + self.manager.gitlab.http_post(path, **kwargs) + + +class TimeTrackingMixin(object): + def time_stats(self, **kwargs): + """Get time stats for the object. + + Raises: + GitlabConnectionError: If the server cannot be reached. + """ + path = '%s/%s/time_stats' % (self.manager.path, self.get_id()) + return self.manager.gitlab.http_get(path, **kwargs) + + def time_estimate(self, duration, **kwargs): + """Set an estimated time of work for the object. + + Args: + duration (str): duration in human format (e.g. 3h30) + + Raises: + GitlabConnectionError: If the server cannot be reached. + """ + path = '%s/%s/time_estimate' % (self.manager.path, self.get_id()) + data = {'duration': duration} + return self.manager.gitlab.http_post(path, post_data=data, **kwargs) + + def reset_time_estimate(self, **kwargs): + """Resets estimated time for the object to 0 seconds. + + Raises: + GitlabConnectionError: If the server cannot be reached. + """ + path = '%s/%s/rest_time_estimate' % (self.manager.path, self.get_id()) + return self.manager.gitlab.http_post(path, **kwargs) + + def add_spent_time(self, duration, **kwargs): + """Add time spent working on the object. + + Args: + duration (str): duration in human format (e.g. 3h30) + + Raises: + GitlabConnectionError: If the server cannot be reached. + """ + path = '%s/%s/add_spent_time' % (self.manager.path, self.get_id()) + data = {'duration': duration} + return self.manager.gitlab.http_post(path, post_data=data, **kwargs) + + def reset_spent_time(self, **kwargs): + """Resets the time spent working on the object. + + Raises: + GitlabConnectionError: If the server cannot be reached. + """ + path = '%s/%s/reset_spent_time' % (self.manager.path, self.get_id()) + return self.manager.gitlab.http_post(path, **kwargs) diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index 8dec461..b547d81 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -22,7 +22,6 @@ import base64 import json import six -from six.moves import urllib import gitlab from gitlab.base import * # noqa @@ -72,7 +71,7 @@ class UserEmailManager(RetrieveMixin, CreateMixin, DeleteMixin, RESTManager): _path = '/users/%(user_id)s/emails' _obj_cls = UserEmail _from_parent_attrs = {'user_id': 'id'} - _create_attrs = {'required': ('email', ), 'optional': tuple()} + _create_attrs = (('email', ), tuple()) class UserKey(RESTObject): @@ -83,7 +82,7 @@ class UserKeyManager(GetFromListMixin, CreateMixin, DeleteMixin, RESTManager): _path = '/users/%(user_id)s/emails' _obj_cls = UserKey _from_parent_attrs = {'user_id': 'id'} - _create_attrs = {'required': ('title', 'key'), 'optional': tuple()} + _create_attrs = (('title', 'key'), tuple()) class UserProject(RESTObject): @@ -94,14 +93,13 @@ class UserProjectManager(CreateMixin, RESTManager): _path = '/projects/user/%(user_id)s' _obj_cls = UserProject _from_parent_attrs = {'user_id': 'id'} - _create_attrs = { - 'required': ('name', ), - 'optional': ('default_branch', 'issues_enabled', 'wall_enabled', - 'merge_requests_enabled', 'wiki_enabled', - 'snippets_enabled', 'public', 'visibility', 'description', - 'builds_enabled', 'public_builds', 'import_url', - 'only_allow_merge_if_build_succeeds') - } + _create_attrs = ( + ('name', ), + ('default_branch', 'issues_enabled', 'wall_enabled', + 'merge_requests_enabled', 'wiki_enabled', 'snippets_enabled', + 'public', 'visibility', 'description', 'builds_enabled', + 'public_builds', 'import_url', 'only_allow_merge_if_build_succeeds') + ) class User(SaveMixin, RESTObject): @@ -143,22 +141,20 @@ class UserManager(CRUDMixin, RESTManager): _list_filters = ('active', 'blocked', 'username', 'extern_uid', 'provider', 'external') - _create_attrs = { - 'required': ('email', 'username', 'name'), - 'optional': ('password', 'reset_password', 'skype', 'linkedin', - 'twitter', 'projects_limit', 'extern_uid', 'provider', - 'bio', 'admin', 'can_create_group', 'website_url', - 'skip_confirmation', 'external', 'organization', - 'location') - } - _update_attrs = { - 'required': ('email', 'username', 'name'), - 'optional': ('password', 'skype', 'linkedin', 'twitter', - 'projects_limit', 'extern_uid', 'provider', 'bio', - 'admin', 'can_create_group', 'website_url', - 'skip_confirmation', 'external', 'organization', - 'location') - } + _create_attrs = ( + ('email', 'username', 'name'), + ('password', 'reset_password', 'skype', 'linkedin', 'twitter', + 'projects_limit', 'extern_uid', 'provider', 'bio', 'admin', + 'can_create_group', 'website_url', 'skip_confirmation', 'external', + 'organization', 'location') + ) + _update_attrs = ( + ('email', 'username', 'name'), + ('password', 'skype', 'linkedin', 'twitter', 'projects_limit', + 'extern_uid', 'provider', 'bio', 'admin', 'can_create_group', + 'website_url', 'skip_confirmation', 'external', 'organization', + 'location') + ) def _sanitize_data(self, data, action): new_data = data.copy() @@ -175,7 +171,7 @@ class CurrentUserEmailManager(RetrieveMixin, CreateMixin, DeleteMixin, RESTManager): _path = '/user/emails' _obj_cls = CurrentUserEmail - _create_attrs = {'required': ('email', ), 'optional': tuple()} + _create_attrs = (('email', ), tuple()) class CurrentUserKey(RESTObject): @@ -186,7 +182,7 @@ class CurrentUserKeyManager(RetrieveMixin, CreateMixin, DeleteMixin, RESTManager): _path = '/user/keys' _obj_cls = CurrentUserKey - _create_attrs = {'required': ('title', 'key'), 'optional': tuple()} + _create_attrs = (('title', 'key'), tuple()) class CurrentUser(RESTObject): @@ -214,21 +210,19 @@ class ApplicationSettings(SaveMixin, RESTObject): 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') - } + _update_attrs = ( + tuple(), + ('after_sign_out_path', 'container_registry_token_expire_delay', + 'default_branch_protection', 'default_project_visibility', + 'default_projects_limit', 'default_snippet_visibility', + 'domain_blacklist', 'domain_blacklist_enabled', 'domain_whitelist', + 'enabled_git_access_protocol', 'gravatar_enabled', 'home_page_url', + 'max_attachment_size', 'repository_storage', + 'restricted_signup_domains', 'restricted_visibility_levels', + 'session_expire_delay', 'sign_in_text', 'signin_enabled', + 'signup_enabled', 'twitter_sharing_enabled', + 'user_oauth_applications') + ) def _sanitize_data(self, data, action): new_data = data.copy() @@ -245,14 +239,9 @@ 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'), - } + _create_attrs = (('message', ), ('starts_at', 'ends_at', 'color', 'font')) + _update_attrs = (tuple(), ('message', 'starts_at', 'ends_at', 'color', + 'font')) class DeployKey(RESTObject): @@ -272,14 +261,13 @@ class NotificationSettingsManager(GetWithoutIdMixin, UpdateMixin, RESTManager): _path = '/notification_settings' _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') - } + _update_attrs = ( + tuple(), + ('level', 'notification_email', 'new_note', 'new_issue', + 'reopen_issue', 'close_issue', 'reassign_issue', 'new_merge_request', + 'reopen_merge_request', 'close_merge_request', + 'reassign_merge_request', 'merge_merge_request') + ) class Dockerfile(RESTObject): @@ -309,128 +297,92 @@ class GitlabciymlManager(RetrieveMixin, RESTManager): _obj_cls = Gitlabciyml -class GroupIssue(GitlabObject): - _url = '/groups/%(group_id)s/issues' - canGet = 'from_list' - canCreate = False - canUpdate = False - canDelete = False - requiredUrlAttrs = ['group_id'] - optionalListAttrs = ['state', 'labels', 'milestone', 'order_by', 'sort'] - - -class GroupIssueManager(BaseManager): - obj_cls = GroupIssue +class GroupIssue(RESTObject): + pass +class GroupIssueManager(GetFromListMixin, RESTManager): + _path = '/groups/%(group_id)s/issues' + _obj_cls = GroupIssue + _from_parent_attrs = {'group_id': 'id'} + _list_filters = ('state', 'labels', 'milestone', 'order_by', 'sort') -class GroupMember(GitlabObject): - _url = '/groups/%(group_id)s/members' - canGet = 'from_list' - requiredUrlAttrs = ['group_id'] - requiredCreateAttrs = ['access_level', 'user_id'] - optionalCreateAttrs = ['expires_at'] - requiredUpdateAttrs = ['access_level'] - optionalCreateAttrs = ['expires_at'] - shortPrintAttr = 'username' - def _update(self, **kwargs): - self.user_id = self.id - super(GroupMember, self)._update(**kwargs) +class GroupMember(SaveMixin, RESTObject): + _short_print_attr = 'username' -class GroupMemberManager(BaseManager): - obj_cls = GroupMember +class GroupMemberManager(GetFromListMixin, CreateMixin, UpdateMixin, + RESTManager): + _path = '/groups/%(group_id)s/members' + _obj_cls = GroupMember + _from_parent_attrs = {'group_id': 'id'} + _create_attrs = (('access_level', 'user_id'), ('expires_at', )) + _update_attrs = (('access_level', ), ('expires_at', )) class GroupNotificationSettings(NotificationSettings): - _url = '/groups/%(group_id)s/notification_settings' - requiredUrlAttrs = ['group_id'] - - -class GroupNotificationSettingsManager(BaseManager): - obj_cls = GroupNotificationSettings - - -class GroupAccessRequest(GitlabObject): - _url = '/groups/%(group_id)s/access_requests' - canGet = 'from_list' - canUpdate = False + pass - def approve(self, access_level=gitlab.DEVELOPER_ACCESS, **kwargs): - """Approve an access request. - Attrs: - access_level (int): The access level for the user. +class GroupNotificationSettingsManager(NotificationSettingsManager): + _path = '/groups/%(group_id)s/notification_settings' + _obj_cls = GroupNotificationSettings + _from_parent_attrs = {'group_id': 'id'} - Raises: - GitlabConnectionError: If the server cannot be reached. - GitlabUpdateError: If the server fails to perform the request. - """ - url = ('/groups/%(group_id)s/access_requests/%(id)s/approve' % - {'group_id': self.group_id, 'id': self.id}) - data = {'access_level': access_level} - r = self.gitlab._raw_put(url, data=data, **kwargs) - raise_error_from_response(r, GitlabUpdateError, 201) - self._set_from_dict(r.json()) +class GroupAccessRequest(AccessRequestMixin, RESTObject): + pass -class GroupAccessRequestManager(BaseManager): - obj_cls = GroupAccessRequest +class GroupAccessRequestManager(GetFromListMixin, CreateMixin, DeleteMixin, + RESTManager): + _path = '/groups/%(group_id)s/access_requests' + _obj_cls = GroupAccessRequest + _from_parent_attrs = {'group_id': 'id'} -class Hook(GitlabObject): +class Hook(RESTObject): _url = '/hooks' - canUpdate = False - requiredCreateAttrs = ['url'] - shortPrintAttr = 'url' + _short_print_attr = 'url' -class HookManager(BaseManager): - obj_cls = Hook +class HookManager(NoUpdateMixin, RESTManager): + _path = '/hooks' + _obj_cls = Hook + _create_attrs = (('url', ), tuple()) -class Issue(GitlabObject): +class Issue(RESTObject): _url = '/issues' - _constructorTypes = {'author': 'User', 'assignee': 'User', - 'milestone': 'ProjectMilestone'} - canGet = 'from_list' - canDelete = False - canUpdate = False - canCreate = False - shortPrintAttr = 'title' - optionalListAttrs = ['state', 'labels', 'order_by', 'sort'] + _constructor_types = {'author': 'User', + 'assignee': 'User', + 'milestone': 'ProjectMilestone'} + _short_print_attr = 'title' -class IssueManager(BaseManager): - obj_cls = Issue +class IssueManager(GetFromListMixin, RESTManager): + _path = '/issues' + _obj_cls = Issue + _list_filters = ('state', 'labels', 'order_by', 'sort') -class License(GitlabObject): - _url = '/templates/licenses' - canDelete = False - canUpdate = False - canCreate = False - idAttr = 'key' +class License(RESTObject): + _id_attr = 'key' - optionalListAttrs = ['popular'] - optionalGetAttrs = ['project', 'fullname'] +class LicenseManager(RetrieveMixin, RESTManager): + _path = '/templates/licenses' + _obj_cls = License + _list_filters =('popular') + _optional_get_attrs = ('project', 'fullname') -class LicenseManager(BaseManager): - obj_cls = License +class Snippet(SaveMixin, RESTObject): + _constructor_types = {'author': 'User'} + _short_print_attr = 'title' -class Snippet(GitlabObject): - _url = '/snippets' - _constructorTypes = {'author': 'User'} - requiredCreateAttrs = ['title', 'file_name', 'content'] - optionalCreateAttrs = ['lifetime', 'visibility'] - optionalUpdateAttrs = ['title', 'file_name', 'content', 'visibility'] - shortPrintAttr = 'title' - - def raw(self, streamed=False, action=None, chunk_size=1024, **kwargs): - """Return the raw content of a snippet. + def content(self, streamed=False, action=None, chunk_size=1024, **kwargs): + """Return the content of a snippet. Args: streamed (bool): If True the data will be processed by chunks of @@ -447,14 +399,19 @@ class Snippet(GitlabObject): GitlabConnectionError: If the server cannot be reached. GitlabGetError: If the server fails to perform the request. """ - url = ("/snippets/%(snippet_id)s/raw" % {'snippet_id': self.id}) - r = self.gitlab._raw_get(url, **kwargs) - raise_error_from_response(r, GitlabGetError) + path = '/snippets/%s/raw' % self.get_id() + result = self.manager.gitlab.http_get(path, streamed=streamed, + **kwargs) return utils.response_content(r, streamed, action, chunk_size) -class SnippetManager(BaseManager): - obj_cls = Snippet +class SnippetManager(CRUDMixin, RESTManager): + _path = '/snippets' + _obj_cls = Snippet + _create_attrs = (('title', 'file_name', 'content'), + ('lifetime', 'visibility')) + _update_attrs = (tuple(), + ('title', 'file_name', 'content', 'visibility')) def public(self, **kwargs): """List all the public snippets. @@ -466,116 +423,101 @@ class SnippetManager(BaseManager): Returns: list(gitlab.Gitlab.Snippet): The list of snippets. """ - return self.gitlab._raw_list("/snippets/public", Snippet, **kwargs) + return self.list(path='/snippets/public', **kwargs) -class Namespace(GitlabObject): - _url = '/namespaces' - canGet = 'from_list' - canUpdate = False - canDelete = False - canCreate = False - optionalListAttrs = ['search'] +class Namespace(RESTObject): + pass -class NamespaceManager(BaseManager): - obj_cls = Namespace +class NamespaceManager(GetFromListMixin, RESTManager): + _path = '/namespaces' + _obj_cls = Namespace + _list_filters = ('search', ) -class ProjectBoardList(GitlabObject): - _url = '/projects/%(project_id)s/boards/%(board_id)s/lists' - requiredUrlAttrs = ['project_id', 'board_id'] - _constructorTypes = {'label': 'ProjectLabel'} - requiredCreateAttrs = ['label_id'] - requiredUpdateAttrs = ['position'] +class ProjectBoardList(SaveMixin, RESTObject): + _constructor_types = {'label': 'ProjectLabel'} -class ProjectBoardListManager(BaseManager): - obj_cls = ProjectBoardList +class ProjectBoardListManager(CRUDMixin, RESTManager): + _path = '/projects/%(project_id)s/boards/%(board_id)s/lists' + _obj_cls = ProjectBoardList + _from_parent_attrs = {'project_id': 'project_id', + 'board_id': 'id'} + _create_attrs = (('label_id', ), tuple()) + _update_attrs = (('position', ), tuple()) -class ProjectBoard(GitlabObject): - _url = '/projects/%(project_id)s/boards' - requiredUrlAttrs = ['project_id'] - _constructorTypes = {'labels': 'ProjectBoardList'} - canGet = 'from_list' - canUpdate = False - canCreate = False - canDelete = False - managers = ( - ('lists', 'ProjectBoardListManager', - [('project_id', 'project_id'), ('board_id', 'id')]), - ) +class ProjectBoard(RESTObject): + _constructor_types = {'labels': 'ProjectBoardList'} + _managers = (('lists', 'ProjectBoardListManager'), ) -class ProjectBoardManager(BaseManager): - obj_cls = ProjectBoard +class ProjectBoardManager(GetFromListMixin, RESTManager): + _path = '/projects/%(project_id)s/boards' + _obj_cls = ProjectBoard + _from_parent_attrs = {'project_id': 'id'} -class ProjectBranch(GitlabObject): - _url = '/projects/%(project_id)s/repository/branches' - _constructorTypes = {'author': 'User', "committer": "User"} +class ProjectBranch(RESTObject): + _constructor_types = {'author': 'User', "committer": "User"} + _id_attr = 'name' - idAttr = 'name' - canUpdate = False - requiredUrlAttrs = ['project_id'] - requiredCreateAttrs = ['branch', 'ref'] - - def protect(self, protect=True, **kwargs): - """Protects the branch.""" - url = self._url % {'project_id': self.project_id} - action = 'protect' if protect else 'unprotect' - url = "%s/%s/%s" % (url, self.name, action) - r = self.gitlab._raw_put(url, data=None, content_type=None, **kwargs) - raise_error_from_response(r, GitlabProtectError) - - if protect: - self.protected = protect - else: - del self.protected + def protect(self, developers_can_push=False, developers_can_merge=False, + **kwargs): + """Protects the branch. + + Args: + developers_can_push (bool): Set to True if developers are allowed + to push to the branch + developers_can_merge (bool): Set to True if developers are allowed + to merge to the branch + """ + path = '%s/%s/protect' % (self.manager.path, self.get_id()) + post_data = {'developers_can_push': developers_can_push, + 'developers_can_merge': developers_can_merge} + self.manager.gitlab.http_put(path, post_data=post_data, **kwargs) + self._attrs['protected'] = True def unprotect(self, **kwargs): """Unprotects the branch.""" - self.protect(False, **kwargs) + path = '%s/%s/protect' % (self.manager.path, self.get_id()) + self.manager.gitlab.http_put(path, **kwargs) + self._attrs['protected'] = False -class ProjectBranchManager(BaseManager): - obj_cls = ProjectBranch +class ProjectBranchManager(NoUpdateMixin, RESTManager): + _path = '/projects/%(project_id)s/repository/branches' + _obj_cls = ProjectBranch + _from_parent_attrs = {'project_id': 'id'} + _create_attrs = (('branch', 'ref'), tuple()) -class ProjectJob(GitlabObject): - _url = '/projects/%(project_id)s/jobs' - _constructorTypes = {'user': 'User', - 'commit': 'ProjectCommit', - 'runner': 'Runner'} - requiredUrlAttrs = ['project_id'] - canDelete = False - canUpdate = False - canCreate = False +class ProjectJob(RESTObject): + _constructor_types = {'user': 'User', + 'commit': 'ProjectCommit', + 'runner': 'Runner'} def cancel(self, **kwargs): """Cancel the job.""" - url = '/projects/%s/jobs/%s/cancel' % (self.project_id, self.id) - r = self.gitlab._raw_post(url) - raise_error_from_response(r, GitlabJobCancelError, 201) + path = '%s/%s/cancel' % (self.manager.path, self.get_id()) + self.manager.gitlab.http_post(path) def retry(self, **kwargs): """Retry the job.""" - url = '/projects/%s/jobs/%s/retry' % (self.project_id, self.id) - r = self.gitlab._raw_post(url) - raise_error_from_response(r, GitlabJobRetryError, 201) + path = '%s/%s/retry' % (self.manager.path, self.get_id()) + self.manager.gitlab.http_post(path) def play(self, **kwargs): """Trigger a job explicitly.""" - url = '/projects/%s/jobs/%s/play' % (self.project_id, self.id) - r = self.gitlab._raw_post(url) - raise_error_from_response(r, GitlabJobPlayError) + path = '%s/%s/play' % (self.manager.path, self.get_id()) + self.manager.gitlab.http_post(path) def erase(self, **kwargs): """Erase the job (remove job artifacts and trace).""" - url = '/projects/%s/jobs/%s/erase' % (self.project_id, self.id) - r = self.gitlab._raw_post(url) - raise_error_from_response(r, GitlabJobEraseError, 201) + path = '%s/%s/erase' % (self.manager.path, self.get_id()) + self.manager.gitlab.http_post(path) def keep_artifacts(self, **kwargs): """Prevent artifacts from being delete when expiration is set. @@ -584,10 +526,8 @@ class ProjectJob(GitlabObject): GitlabConnectionError: If the server cannot be reached. GitlabCreateError: If the request failed. """ - url = ('/projects/%s/jobs/%s/artifacts/keep' % - (self.project_id, self.id)) - r = self.gitlab._raw_post(url) - raise_error_from_response(r, GitlabGetError, 200) + path = '%s/%s/artifacts/keep' % (self.manager.path, self.get_id()) + self.manager.gitlab.http_post(path) def artifacts(self, streamed=False, action=None, chunk_size=1024, **kwargs): @@ -608,10 +548,10 @@ class ProjectJob(GitlabObject): GitlabConnectionError: If the server cannot be reached. GitlabGetError: If the artifacts are not available. """ - url = '/projects/%s/jobs/%s/artifacts' % (self.project_id, self.id) - r = self.gitlab._raw_get(url, streamed=streamed, **kwargs) - raise_error_from_response(r, GitlabGetError, 200) - return utils.response_content(r, streamed, action, chunk_size) + path = '%s/%s/artifacts' % (self.manager.path, self.get_id()) + result = self.manager.gitlab.get_http(path, streamed=streamed, + **kwargs) + return utils.response_content(result, streamed, action, chunk_size) def trace(self, streamed=False, action=None, chunk_size=1024, **kwargs): """Get the job trace. @@ -631,96 +571,70 @@ class ProjectJob(GitlabObject): GitlabConnectionError: If the server cannot be reached. GitlabGetError: If the trace is not available. """ - url = '/projects/%s/jobs/%s/trace' % (self.project_id, self.id) - r = self.gitlab._raw_get(url, streamed=streamed, **kwargs) - raise_error_from_response(r, GitlabGetError, 200) - return utils.response_content(r, streamed, action, chunk_size) + path = '%s/%s/trace' % (self.manager.path, self.get_id()) + result = self.manager.gitlab.get_http(path, streamed=streamed, + **kwargs) + return utils.response_content(result, streamed, action, chunk_size) -class ProjectJobManager(BaseManager): - obj_cls = ProjectJob +class ProjectJobManager(RetrieveMixin, RESTManager): + _path = '/projects/%(project_id)s/jobs' + _obj_cls = ProjectJob + _from_parent_attrs = {'project_id': 'id'} -class ProjectCommitStatus(GitlabObject): - _url = '/projects/%(project_id)s/repository/commits/%(commit_id)s/statuses' - _create_url = '/projects/%(project_id)s/statuses/%(commit_id)s' - canUpdate = False - canDelete = False - requiredUrlAttrs = ['project_id', 'commit_id'] - optionalGetAttrs = ['ref_name', 'stage', 'name', 'all'] - requiredCreateAttrs = ['state'] - optionalCreateAttrs = ['description', 'name', 'context', 'ref', - 'target_url'] - - -class ProjectCommitStatusManager(BaseManager): - obj_cls = ProjectCommitStatus +class ProjectCommitStatus(RESTObject): + pass -class ProjectCommitComment(GitlabObject): - _url = '/projects/%(project_id)s/repository/commits/%(commit_id)s/comments' - canUpdate = False - canGet = False - canDelete = False - requiredUrlAttrs = ['project_id', 'commit_id'] - requiredCreateAttrs = ['note'] - optionalCreateAttrs = ['path', 'line', 'line_type'] +class ProjectCommitStatusManager(RetrieveMixin, CreateMixin, RESTManager): + _path = '/projects/%(project_id)s/repository/commits/%(commit_id)s/statuses' + _obj_cls = ProjectCommitStatus + _from_parent_attrs = {'project_id': 'project_id', 'commit_id': 'id'} + _create_attrs = (('state', ), + ('description', 'name', 'context', 'ref', 'target_url')) + def create(self, data, **kwargs): + """Creates a new object. -class ProjectCommitCommentManager(BaseManager): - obj_cls = ProjectCommitComment + Args: + data (dict): parameters to send to the server to create the + resource + **kwargs: Extra data to send to the Gitlab server (e.g. sudo or + 'ref_name', 'stage', 'name', 'all'. + Returns: + RESTObject: a new instance of the manage object class build with + the data sent by the server + """ + path = '/projects/%(project_id)s/statuses/%(commit_id)s' + computed_path = self._compute_path(path) + return CreateMixin.create(self, data, path=computed_path, **kwargs) -class ProjectCommit(GitlabObject): - _url = '/projects/%(project_id)s/repository/commits' - canDelete = False - canUpdate = False - requiredUrlAttrs = ['project_id'] - requiredCreateAttrs = ['branch', 'commit_message', 'actions'] - optionalCreateAttrs = ['author_email', 'author_name'] - shortPrintAttr = 'title' - managers = ( - ('comments', 'ProjectCommitCommentManager', - [('project_id', 'project_id'), ('commit_id', 'id')]), - ('statuses', 'ProjectCommitStatusManager', - [('project_id', 'project_id'), ('commit_id', 'id')]), - ) - def diff(self, **kwargs): - """Generate the commit diff.""" - url = ('/projects/%(project_id)s/repository/commits/%(commit_id)s/diff' - % {'project_id': self.project_id, 'commit_id': self.id}) - r = self.gitlab._raw_get(url, **kwargs) - raise_error_from_response(r, GitlabGetError) +class ProjectCommitComment(RESTObject): + pass - return r.json() - def blob(self, filepath, streamed=False, action=None, chunk_size=1024, - **kwargs): - """Generate the content of a file for this commit. +class ProjectCommitCommentManager(ListMixin, CreateMixin, RESTManager): + _path = ('/projects/%(project_id)s/repository/commits/%(commit_id)s' + '/comments') + _obj_cls = ProjectCommitComment + _from_parent_attrs = {'project_id': 'project_id', 'commit_id': 'id'} + _create_attrs = (('note', ), ('path', 'line', 'line_type')) - Args: - filepath (str): Path of the file to request. - streamed (bool): If True the data will be processed by chunks of - `chunk_size` and each chunk is passed to `action` for - treatment. - action (callable): Callable responsible of dealing with chunk of - data. - chunk_size (int): Size of each chunk. - Returns: - str: The content of the file +class ProjectCommit(RESTObject): + _short_print_attr = 'title' + _managers = ( + ('comments', 'ProjectCommitCommentManager'), + ('statuses', 'ProjectCommitStatusManager'), + ) - Raises: - GitlabConnectionError: If the server cannot be reached. - GitlabGetError: If the server fails to perform the request. - """ - url = ('/projects/%(project_id)s/repository/blobs/%(commit_id)s' % - {'project_id': self.project_id, 'commit_id': self.id}) - url += '?filepath=%s' % filepath - r = self.gitlab._raw_get(url, streamed=streamed, **kwargs) - raise_error_from_response(r, GitlabGetError) - return utils.response_content(r, streamed, action, chunk_size) + def diff(self, **kwargs): + """Generate the commit diff.""" + path = '%s/%s/diff' % (self.manager.path, self.get_id()) + return self.manager.gitlab.http_get(path, **kwargs) def cherry_pick(self, branch, **kwargs): """Cherry-pick a commit into a branch. @@ -731,151 +645,121 @@ class ProjectCommit(GitlabObject): Raises: GitlabCherryPickError: If the cherry pick could not be applied. """ - url = ('/projects/%s/repository/commits/%s/cherry_pick' % - (self.project_id, self.id)) + path = '%s/%s/cherry_pick' % (self.manager.path, self.get_id()) + post_data = {'branch': branch} + self.manager.gitlab.http_post(path, post_data=post_data, **kwargs) - r = self.gitlab._raw_post(url, data={'project_id': self.project_id, - 'branch': branch}, **kwargs) - errors = {400: GitlabCherryPickError} - raise_error_from_response(r, errors, expected_code=201) +class ProjectCommitManager(RetrieveMixin, CreateMixin, RESTManager): + _path = '/projects/%(project_id)s/repository/commits' + _obj_cls = ProjectCommit + _from_parent_attrs = {'project_id': 'id'} + _create_attrs = (('branch', 'commit_message', 'actions'), + ('author_email', 'author_name')) -class ProjectCommitManager(BaseManager): - obj_cls = ProjectCommit +class ProjectEnvironment(SaveMixin, RESTObject): + pass -class ProjectEnvironment(GitlabObject): - _url = '/projects/%(project_id)s/environments' - canGet = 'from_list' - requiredUrlAttrs = ['project_id'] - requiredCreateAttrs = ['name'] - optionalCreateAttrs = ['external_url'] - optionalUpdateAttrs = ['name', 'external_url'] +class ProjectEnvironmentManager(GetFromListMixin, CreateMixin, UpdateMixin, + DeleteMixin, RESTManager): + _path = '/projects/%(project_id)s/environments' + _obj_cls = ProjectEnvironment + _from_parent_attrs = {'project_id': 'id'} + _create_attrs = (('name', ), ('external_url', )) + _update_attrs = (tuple(), ('name', 'external_url')) -class ProjectEnvironmentManager(BaseManager): - obj_cls = ProjectEnvironment +class ProjectKey(RESTObject): + pass -class ProjectKey(GitlabObject): - _url = '/projects/%(project_id)s/deploy_keys' - canUpdate = False - requiredUrlAttrs = ['project_id'] - requiredCreateAttrs = ['title', 'key'] +class ProjectKeyManager(NoUpdateMixin, RESTManager): + _path = '/projects/%(project_id)s/deploy_keys' + _obj_cls = ProjectKey + _from_parent_attrs = {'project_id': 'id'} + _create_attrs = (('title', 'key'), tuple()) -class ProjectKeyManager(BaseManager): - obj_cls = ProjectKey + def enable(self, key_id, **kwargs): + """Enable a deploy key for a project. - def enable(self, key_id): - """Enable a deploy key for a project.""" - url = '/projects/%s/deploy_keys/%s/enable' % (self.parent.id, key_id) - r = self.gitlab._raw_post(url) - raise_error_from_response(r, GitlabProjectDeployKeyError, 201) + Args: + key_id (int): The ID of the key to enable + """ + path = '%s/%s/enable' % (self.manager.path, key_id) + self.manager.gitlab.http_post(path, **kwargs) -class ProjectEvent(GitlabObject): - _url = '/projects/%(project_id)s/events' - canGet = 'from_list' - canDelete = False - canUpdate = False - canCreate = False - requiredUrlAttrs = ['project_id'] - shortPrintAttr = 'target_title' +class ProjectEvent(RESTObject): + _short_print_attr = 'target_title' -class ProjectEventManager(BaseManager): - obj_cls = ProjectEvent +class ProjectEventManager(GetFromListMixin, RESTManager): + _path ='/projects/%(project_id)s/events' + _obj_cls = ProjectEvent + _from_parent_attrs = {'project_id': 'id'} -class ProjectFork(GitlabObject): - _url = '/projects/%(project_id)s/fork' - canUpdate = False - canDelete = False - canList = False - canGet = False - requiredUrlAttrs = ['project_id'] - optionalCreateAttrs = ['namespace'] +class ProjectFork(RESTObject): + pass -class ProjectForkManager(BaseManager): - obj_cls = ProjectFork +class ProjectForkManager(CreateMixin, RESTManager): + _path = '/projects/%(project_id)s/fork' + _obj_cls = ProjectFork + _from_parent_attrs = {'project_id': 'id'} + _create_attrs = (tuple(), ('namespace', )) -class ProjectHook(GitlabObject): - _url = '/projects/%(project_id)s/hooks' +class ProjectHook(SaveMixin, RESTObject): requiredUrlAttrs = ['project_id'] requiredCreateAttrs = ['url'] optionalCreateAttrs = ['push_events', 'issues_events', 'note_events', 'merge_requests_events', 'tag_push_events', 'build_events', 'enable_ssl_verification', 'token', 'pipeline_events'] - shortPrintAttr = 'url' - - -class ProjectHookManager(BaseManager): - obj_cls = ProjectHook - - -class ProjectIssueNote(GitlabObject): - _url = '/projects/%(project_id)s/issues/%(issue_iid)s/notes' - _constructorTypes = {'author': 'User'} - canDelete = False - requiredUrlAttrs = ['project_id', 'issue_iid'] - requiredCreateAttrs = ['body'] - optionalCreateAttrs = ['created_at'] - - -class ProjectIssueNoteManager(BaseManager): - obj_cls = ProjectIssueNote - - -class ProjectIssue(GitlabObject): - _url = '/projects/%(project_id)s/issues/' - _constructorTypes = {'author': 'User', 'assignee': 'User', - 'milestone': 'ProjectMilestone'} - optionalListAttrs = ['state', 'labels', 'milestone', 'order_by', 'sort'] - requiredUrlAttrs = ['project_id'] - requiredCreateAttrs = ['title'] - optionalCreateAttrs = ['description', 'assignee_id', 'milestone_id', - 'labels', 'created_at', 'due_date'] - optionalUpdateAttrs = ['title', 'description', 'assignee_id', - 'milestone_id', 'labels', 'created_at', - 'updated_at', 'state_event', 'due_date'] - shortPrintAttr = 'title' - idAttr = 'iid' - managers = ( - ('notes', 'ProjectIssueNoteManager', - [('project_id', 'project_id'), ('issue_iid', 'iid')]), + _short_print_attr = 'url' + + +class ProjectHookManager(CRUDMixin, RESTManager): + _path = '/projects/%(project_id)s/hooks' + _obj_cls = ProjectHook + _from_parent_attrs = {'project_id': 'id'} + _create_attrs = ( + ('url', ), + ('push_events', 'issues_events', 'note_events', + 'merge_requests_events', 'tag_push_events', 'build_events', + 'enable_ssl_verification', 'token', 'pipeline_events') + ) + _update_attrs = ( + ('url', ), + ('push_events', 'issues_events', 'note_events', + 'merge_requests_events', 'tag_push_events', 'build_events', + 'enable_ssl_verification', 'token', 'pipeline_events') ) - def subscribe(self, **kwargs): - """Subscribe to an issue. - Raises: - GitlabConnectionError: If the server cannot be reached. - GitlabSubscribeError: If the subscription cannot be done - """ - url = ('/projects/%(project_id)s/issues/%(issue_iid)s/subscribe' % - {'project_id': self.project_id, 'issue_iid': self.iid}) +class ProjectIssueNote(SaveMixin, RESTObject): + _constructor_types= {'author': 'User'} - r = self.gitlab._raw_post(url, **kwargs) - raise_error_from_response(r, GitlabSubscribeError, [201, 304]) - self._set_from_dict(r.json()) - def unsubscribe(self, **kwargs): - """Unsubscribe an issue. +class ProjectIssueNoteManager(RetrieveMixin, CreateMixin, UpdateMixin, + RESTManager): + _path = '/projects/%(project_id)s/issues/%(issue_iid)s/notes' + _obj_cls = ProjectIssueNote + _from_parent_attrs = {'project_id': 'project_id', 'issue_iid': 'iid'} + _create_attrs = (('body', ), ('created_at')) + _update_attrs = (('body', ), tuple()) - Raises: - GitlabConnectionError: If the server cannot be reached. - GitlabUnsubscribeError: If the unsubscription cannot be done - """ - url = ('/projects/%(project_id)s/issues/%(issue_iid)s/unsubscribe' % - {'project_id': self.project_id, 'issue_iid': self.iid}) - r = self.gitlab._raw_post(url, **kwargs) - raise_error_from_response(r, GitlabUnsubscribeError, [201, 304]) - self._set_from_dict(r.json()) +class ProjectIssue(SubscribableMixin, TodoMixin, TimeTrackingMixin, SaveMixin, + RESTObject): + _constructor_types = {'author': 'User', 'assignee': 'User', 'milestone': + 'ProjectMilestone'} + _short_print_attr = 'title' + _id_attr = 'iid' + _managers = (('notes', 'ProjectIssueNoteManager'), ) def move(self, to_project_id, **kwargs): """Move the issue to another project. @@ -883,160 +767,70 @@ class ProjectIssue(GitlabObject): Raises: GitlabConnectionError: If the server cannot be reached. """ - url = ('/projects/%(project_id)s/issues/%(issue_iid)s/move' % - {'project_id': self.project_id, 'issue_iid': self.iid}) - + path = '%s/%s/move' % (self.manager.path, self.get_id()) data = {'to_project_id': to_project_id} - data.update(**kwargs) - r = self.gitlab._raw_post(url, data=data) - raise_error_from_response(r, GitlabUpdateError, 201) - self._set_from_dict(r.json()) + server_data = self.manager.gitlab.http_post(url, post_data=data, + **kwargs) + self._update_attrs(server_data) - def todo(self, **kwargs): - """Create a todo for the issue. - Raises: - GitlabConnectionError: If the server cannot be reached. - """ - url = ('/projects/%(project_id)s/issues/%(issue_iid)s/todo' % - {'project_id': self.project_id, 'issue_iid': self.iid}) - r = self.gitlab._raw_post(url, **kwargs) - raise_error_from_response(r, GitlabTodoError, [201, 304]) - - def time_stats(self, **kwargs): - """Get time stats for the issue. - - Raises: - GitlabConnectionError: If the server cannot be reached. - """ - url = ('/projects/%(project_id)s/issues/%(issue_iid)s/time_stats' % - {'project_id': self.project_id, 'issue_iid': self.iid}) - r = self.gitlab._raw_get(url, **kwargs) - raise_error_from_response(r, GitlabGetError) - return r.json() +class ProjectIssueManager(CRUDMixin, RESTManager): + _path = '/projects/%(project_id)s/issues/' + _obj_cls = ProjectIssue + _from_parent_attrs = {'project_id': 'id'} + _list_filters = ('state', 'labels', 'milestone', 'order_by', 'sort') + _create_attrs = (('title', ), + ('description', 'assignee_id', 'milestone_id', 'labels', + 'created_at', 'due_date')) + _update_attrs = (tuple(), ('title', 'description', 'assignee_id', + 'milestone_id', 'labels', 'created_at', + 'updated_at', 'state_event', 'due_date')) - def time_estimate(self, duration, **kwargs): - """Set an estimated time of work for the issue. - Args: - duration (str): duration in human format (e.g. 3h30) - - Raises: - GitlabConnectionError: If the server cannot be reached. - """ - url = ('/projects/%(project_id)s/issues/%(issue_iid)s/time_estimate' % - {'project_id': self.project_id, 'issue_iid': self.iid}) - data = {'duration': duration} - r = self.gitlab._raw_post(url, data, **kwargs) - raise_error_from_response(r, GitlabTimeTrackingError, 200) - return r.json() - - def reset_time_estimate(self, **kwargs): - """Resets estimated time for the issue to 0 seconds. - - Raises: - GitlabConnectionError: If the server cannot be reached. - """ - url = ('/projects/%(project_id)s/issues/%(issue_iid)s/' - 'reset_time_estimate' % - {'project_id': self.project_id, 'issue_iid': self.iid}) - r = self.gitlab._raw_post(url, **kwargs) - raise_error_from_response(r, GitlabTimeTrackingError, 200) - return r.json() - - def add_spent_time(self, duration, **kwargs): - """Set an estimated time of work for the issue. - - Args: - duration (str): duration in human format (e.g. 3h30) - - Raises: - GitlabConnectionError: If the server cannot be reached. - """ - url = ('/projects/%(project_id)s/issues/%(issue_iid)s/' - 'add_spent_time' % - {'project_id': self.project_id, 'issue_iid': self.iid}) - data = {'duration': duration} - r = self.gitlab._raw_post(url, data, **kwargs) - raise_error_from_response(r, GitlabTimeTrackingError, 201) - return r.json() - - def reset_spent_time(self, **kwargs): - """Set an estimated time of work for the issue. - - Raises: - GitlabConnectionError: If the server cannot be reached. - """ - url = ('/projects/%(project_id)s/issues/%(issue_iid)s/' - 'reset_spent_time' % - {'project_id': self.project_id, 'issue_iid': self.iid}) - r = self.gitlab._raw_post(url, **kwargs) - raise_error_from_response(r, GitlabTimeTrackingError, 200) - return r.json() - - -class ProjectIssueManager(BaseManager): - obj_cls = ProjectIssue - - -class ProjectMember(GitlabObject): - _url = '/projects/%(project_id)s/members' - requiredUrlAttrs = ['project_id'] +class ProjectMember(SaveMixin, RESTObject): requiredCreateAttrs = ['access_level', 'user_id'] optionalCreateAttrs = ['expires_at'] requiredUpdateAttrs = ['access_level'] optionalCreateAttrs = ['expires_at'] - shortPrintAttr = 'username' + _short_print_attr = 'username' -class ProjectMemberManager(BaseManager): - obj_cls = ProjectMember +class ProjectMemberManager(CRUDMixin, RESTManager): + _path = '/projects/%(project_id)s/members' + _obj_cls = ProjectMember + _from_parent_attrs = {'project_id': 'id'} + _create_attrs = (('access_level', 'user_id'), ('expires_at', )) + _update_attrs = (('access_level', ), ('expires_at', )) -class ProjectNote(GitlabObject): - _url = '/projects/%(project_id)s/notes' - _constructorTypes = {'author': 'User'} - canUpdate = False - canDelete = False - requiredUrlAttrs = ['project_id'] - requiredCreateAttrs = ['body'] +class ProjectNote(RESTObject): + _constructor_types = {'author': 'User'} -class ProjectNoteManager(BaseManager): - obj_cls = ProjectNote +class ProjectNoteManager(RetrieveMixin, RESTManager): + _path ='/projects/%(project_id)s/notes' + _obj_cls = ProjectNote + _from_parent_attrs = {'project_id': 'id'} + _create_attrs = (('body', ), tuple()) class ProjectNotificationSettings(NotificationSettings): - _url = '/projects/%(project_id)s/notification_settings' - requiredUrlAttrs = ['project_id'] - - -class ProjectNotificationSettingsManager(BaseManager): - obj_cls = ProjectNotificationSettings + pass -class ProjectTagRelease(GitlabObject): - _url = '/projects/%(project_id)s/repository/tags/%(tag_name)/release' - canDelete = False - canList = False - requiredUrlAttrs = ['project_id', 'tag_name'] - requiredCreateAttrs = ['description'] - shortPrintAttr = 'description' +class ProjectNotificationSettingsManager(NotificationSettingsManager): + _path = '/projects/%(project_id)s/notification_settings' + _obj_cls = ProjectNotificationSettings + _from_parent_attrs = {'project_id': 'id'} -class ProjectTag(GitlabObject): - _url = '/projects/%(project_id)s/repository/tags' - _constructorTypes = {'release': 'ProjectTagRelease', - 'commit': 'ProjectCommit'} - idAttr = 'name' - canGet = 'from_list' - canUpdate = False - requiredUrlAttrs = ['project_id'] - requiredCreateAttrs = ['tag_name', 'ref'] - optionalCreateAttrs = ['message'] - shortPrintAttr = 'name' +class ProjectTag(RESTObject): + _constructor_types = {'release': 'ProjectTagRelease', + 'commit': 'ProjectCommit'} + _id_attr = 'name' + _short_print_attr = 'name' - def set_release_description(self, description): + def set_release_description(self, description, **kwargs): """Set the release notes on the tag. If the release doesn't exist yet, it will be created. If it already @@ -1050,121 +844,64 @@ class ProjectTag(GitlabObject): GitlabCreateError: If the server fails to create the release. GitlabUpdateError: If the server fails to update the release. """ - url = '/projects/%s/repository/tags/%s/release' % (self.project_id, - self.name) + _path = '%s/%s/release' % (self.manager.path, self.get_id()) + data = {'description': description} if self.release is None: - r = self.gitlab._raw_post(url, data={'description': description}) - raise_error_from_response(r, GitlabCreateError, 201) + result = self.manager.gitlab.http_post(url, post_data=data, + **kwargs) else: - r = self.gitlab._raw_put(url, data={'description': description}) - raise_error_from_response(r, GitlabUpdateError, 200) - self.release = ProjectTagRelease(self, r.json()) - + result = self.manager.gitlab.http_put(url, post_data=data, + **kwargs) + self.release = result.json() -class ProjectTagManager(BaseManager): - obj_cls = ProjectTag +class ProjectTagManager(GetFromListMixin, CreateMixin, DeleteMixin, + RESTManager): + _path = '/projects/%(project_id)s/repository/tags' + _obj_cls = ProjectTag + _from_parent_attrs = {'project_id': 'id'} + _create_attrs = (('tag_name', 'ref'), ('message',)) -class ProjectMergeRequestDiff(GitlabObject): - _url = ('/projects/%(project_id)s/merge_requests/' - '%(merge_request_iid)s/versions') - canCreate = False - canUpdate = False - canDelete = False - requiredUrlAttrs = ['project_id', 'merge_request_iid'] - -class ProjectMergeRequestDiffManager(BaseManager): - obj_cls = ProjectMergeRequestDiff - - -class ProjectMergeRequestNote(GitlabObject): - _url = ('/projects/%(project_id)s/merge_requests/%(merge_request_iid)s' - '/notes') - _constructorTypes = {'author': 'User'} - requiredUrlAttrs = ['project_id', 'merge_request_iid'] - requiredCreateAttrs = ['body'] - - -class ProjectMergeRequestNoteManager(BaseManager): - obj_cls = ProjectMergeRequestNote +class ProjectMergeRequestDiff(RESTObject): + pass -class ProjectMergeRequest(GitlabObject): - _url = '/projects/%(project_id)s/merge_requests' - _constructorTypes = {'author': 'User', 'assignee': 'User'} - requiredUrlAttrs = ['project_id'] - requiredCreateAttrs = ['source_branch', 'target_branch', 'title'] - optionalCreateAttrs = ['assignee_id', 'description', 'target_project_id', - 'labels', 'milestone_id', 'remove_source_branch'] - optionalUpdateAttrs = ['target_branch', 'assignee_id', 'title', - 'description', 'state_event', 'labels', - 'milestone_id'] - optionalListAttrs = ['iids', 'state', 'order_by', 'sort'] - idAttr = 'iid' - - managers = ( - ('notes', 'ProjectMergeRequestNoteManager', - [('project_id', 'project_id'), ('merge_request_iid', 'iid')]), - ('diffs', 'ProjectMergeRequestDiffManager', - [('project_id', 'project_id'), ('merge_request_iid', 'iid')]), - ) +class ProjectMergeRequestDiffManager(RetrieveMixin, RESTManager): + _path = '/projects/%(project_id)s/merge_requests/%(mr_iid)s/versions' + _obj_cls = ProjectMergeRequestDiff + _from_parent_attrs = {'project_id': 'project_id', 'mr_iid': 'iid'} - def _data_for_gitlab(self, extra_parameters={}, update=False, - as_json=True): - data = (super(ProjectMergeRequest, self) - ._data_for_gitlab(extra_parameters, update=update, - as_json=False)) - if update: - # Drop source_branch attribute as it is not accepted by the gitlab - # server (Issue #76) - data.pop('source_branch', None) - return json.dumps(data) - def subscribe(self, **kwargs): - """Subscribe to a MR. +class ProjectMergeRequestNote(SaveMixin, RESTObject): + _constructor_types = {'author': 'User'} - Raises: - GitlabConnectionError: If the server cannot be reached. - GitlabSubscribeError: If the subscription cannot be done - """ - url = ('/projects/%(project_id)s/merge_requests/%(mr_iid)s/' - 'subscribe' % - {'project_id': self.project_id, 'mr_iid': self.iid}) - r = self.gitlab._raw_post(url, **kwargs) - raise_error_from_response(r, GitlabSubscribeError, [201, 304]) - if r.status_code == 201: - self._set_from_dict(r.json()) +class ProjectMergeRequestNoteManager(CRUDMixin, RESTManager): + _path = '/projects/%(project_id)s/merge_requests/%(mr_iid)s/notes' + _obj_cls = ProjectMergeRequestNote + _from_parent_attrs = {'project_id': 'project_id', 'mr_iid': 'iid'} + _create_attrs = (('body', ), tuple()) + _update_attrs = (('body', ), tuple()) - def unsubscribe(self, **kwargs): - """Unsubscribe a MR. - Raises: - GitlabConnectionError: If the server cannot be reached. - GitlabUnsubscribeError: If the unsubscription cannot be done - """ - url = ('/projects/%(project_id)s/merge_requests/%(mr_iid)s/' - 'unsubscribe' % - {'project_id': self.project_id, 'mr_iid': self.iid}) +class ProjectMergeRequest(SubscribableMixin, TodoMixin, TimeTrackingMixin, + SaveMixin, RESTObject): + _constructor_types = {'author': 'User', 'assignee': 'User'} + _id_attr = 'iid' - r = self.gitlab._raw_post(url, **kwargs) - raise_error_from_response(r, GitlabUnsubscribeError, [201, 304]) - if r.status_code == 200: - self._set_from_dict(r.json()) + _managers = ( + ('notes', 'ProjectMergeRequestNoteManager'), + ('diffs', 'ProjectMergeRequestDiffManager') + ) def cancel_merge_when_pipeline_succeeds(self, **kwargs): """Cancel merge when build succeeds.""" - u = ('/projects/%s/merge_requests/%s/' - 'cancel_merge_when_pipeline_succeeds' - % (self.project_id, self.iid)) - r = self.gitlab._raw_put(u, **kwargs) - errors = {401: GitlabMRForbiddenError, - 405: GitlabMRClosedError, - 406: GitlabMROnBuildSuccessError} - raise_error_from_response(r, errors) - return ProjectMergeRequest(self, r.json()) + path = ('%s/%s/cancel_merge_when_pipeline_succeeds' % + (self.manager.path, self.get_id())) + server_data = self.manager.gitlab.http_put(path, **kwargs) + self._update_attrs(server_data) def closes_issues(self, **kwargs): """List issues closed by the MR. @@ -1176,6 +913,7 @@ class ProjectMergeRequest(GitlabObject): GitlabConnectionError: If the server cannot be reached. GitlabGetError: If the server fails to perform the request. """ + # FIXME(gpocentek) url = ('/projects/%s/merge_requests/%s/closes_issues' % (self.project_id, self.iid)) return self.gitlab._raw_list(url, ProjectIssue, **kwargs) @@ -1190,6 +928,7 @@ class ProjectMergeRequest(GitlabObject): GitlabConnectionError: If the server cannot be reached. GitlabListError: If the server fails to perform the request. """ + # FIXME(gpocentek) url = ('/projects/%s/merge_requests/%s/commits' % (self.project_id, self.iid)) return self.gitlab._raw_list(url, ProjectCommit, **kwargs) @@ -1204,11 +943,8 @@ class ProjectMergeRequest(GitlabObject): GitlabConnectionError: If the server cannot be reached. GitlabListError: If the server fails to perform the request. """ - url = ('/projects/%s/merge_requests/%s/changes' % - (self.project_id, self.iid)) - r = self.gitlab._raw_get(url, **kwargs) - raise_error_from_response(r, GitlabListError) - return r.json() + path = '%s/%s/changes' % (self.manager.path, self.get_id()) + return self.manager.gitlab.http_get(path, **kwargs) def merge(self, merge_commit_message=None, should_remove_source_branch=False, @@ -1231,8 +967,7 @@ class ProjectMergeRequest(GitlabObject): close thr MR GitlabMRClosedError: If the MR is already closed """ - url = '/projects/%s/merge_requests/%s/merge' % (self.project_id, - self.iid) + path = '%s/%s/merge' % (self.manager.path, self.get_id()) data = {} if merge_commit_message: data['merge_commit_message'] = merge_commit_message @@ -1241,114 +976,31 @@ class ProjectMergeRequest(GitlabObject): if merged_when_build_succeeds: data['merged_when_build_succeeds'] = True - r = self.gitlab._raw_put(url, data=data, **kwargs) - errors = {401: GitlabMRForbiddenError, - 405: GitlabMRClosedError} - raise_error_from_response(r, errors) - self._set_from_dict(r.json()) - - def todo(self, **kwargs): - """Create a todo for the merge request. - - Raises: - GitlabConnectionError: If the server cannot be reached. - """ - url = ('/projects/%(project_id)s/merge_requests/%(mr_iid)s/todo' % - {'project_id': self.project_id, 'mr_iid': self.iid}) - r = self.gitlab._raw_post(url, **kwargs) - raise_error_from_response(r, GitlabTodoError, [201, 304]) - - def time_stats(self, **kwargs): - """Get time stats for the merge request. - - Raises: - GitlabConnectionError: If the server cannot be reached. - """ - url = ('/projects/%(project_id)s/merge_requests/%(mr_iid)s/' - 'time_stats' % - {'project_id': self.project_id, 'mr_iid': self.iid}) - r = self.gitlab._raw_get(url, **kwargs) - raise_error_from_response(r, GitlabGetError) - return r.json() - - def time_estimate(self, duration, **kwargs): - """Set an estimated time of work for the merge request. - - Args: - duration (str): duration in human format (e.g. 3h30) - - Raises: - GitlabConnectionError: If the server cannot be reached. - """ - url = ('/projects/%(project_id)s/merge_requests/%(mr_iid)s/' - 'time_estimate' % - {'project_id': self.project_id, 'mr_iid': self.iid}) - data = {'duration': duration} - r = self.gitlab._raw_post(url, data, **kwargs) - raise_error_from_response(r, GitlabTimeTrackingError, 200) - return r.json() - - def reset_time_estimate(self, **kwargs): - """Resets estimated time for the merge request to 0 seconds. - - Raises: - GitlabConnectionError: If the server cannot be reached. - """ - url = ('/projects/%(project_id)s/merge_requests/%(mr_iid)s/' - 'reset_time_estimate' % - {'project_id': self.project_id, 'mr_iid': self.iid}) - r = self.gitlab._raw_post(url, **kwargs) - raise_error_from_response(r, GitlabTimeTrackingError, 200) - return r.json() - - def add_spent_time(self, duration, **kwargs): - """Set an estimated time of work for the merge request. - - Args: - duration (str): duration in human format (e.g. 3h30) - - Raises: - GitlabConnectionError: If the server cannot be reached. - """ - url = ('/projects/%(project_id)s/merge_requests/%(mr_iid)s/' - 'add_spent_time' % - {'project_id': self.project_id, 'mr_iid': self.iid}) - data = {'duration': duration} - r = self.gitlab._raw_post(url, data, **kwargs) - raise_error_from_response(r, GitlabTimeTrackingError, 201) - return r.json() - - def reset_spent_time(self, **kwargs): - """Set an estimated time of work for the merge request. + server_data = self.manager.gitlab.http_put(path, post_data=data, + **kwargs) + self._update_attrs(server_data) - Raises: - GitlabConnectionError: If the server cannot be reached. - """ - url = ('/projects/%(project_id)s/merge_requests/%(mr_iid)s/' - 'reset_spent_time' % - {'project_id': self.project_id, 'mr_iid': self.iid}) - r = self.gitlab._raw_post(url, **kwargs) - raise_error_from_response(r, GitlabTimeTrackingError, 200) - return r.json() - -class ProjectMergeRequestManager(BaseManager): - obj_cls = ProjectMergeRequest +class ProjectMergeRequestManager(CRUDMixin, RESTManager): + _path = '/projects/%(project_id)s/merge_requests' + _obj_cls = ProjectMergeRequest + _from_parent_attrs = {'project_id': 'id'} + _create_attrs = ( + ('source_branch', 'target_branch', 'title'), + ('assignee_id', 'description', 'target_project_id', 'labels', + 'milestone_id', 'remove_source_branch') + ) + _update_attrs = (tuple(), ('target_branch', 'assignee_id', 'title', + 'description', 'state_event', 'labels', + 'milestone_id')) + _list_filters = ('iids', 'state', 'order_by', 'sort') -class ProjectMilestone(GitlabObject): - _url = '/projects/%(project_id)s/milestones' - canDelete = False - requiredUrlAttrs = ['project_id'] - optionalListAttrs = ['iids', 'state'] - requiredCreateAttrs = ['title'] - optionalCreateAttrs = ['description', 'due_date', 'start_date', - 'state_event'] - optionalUpdateAttrs = requiredCreateAttrs + optionalCreateAttrs - shortPrintAttr = 'title' +class ProjectMilestone(SaveMixin, RESTObject): + _short_print_attr = 'title' def issues(self, **kwargs): - url = "/projects/%s/milestones/%s/issues" % (self.project_id, self.id) + url = '/projects/%s/milestones/%s/issues' % (self.project_id, self.id) return self.gitlab._raw_list(url, ProjectIssue, **kwargs) def merge_requests(self, **kwargs): @@ -1361,71 +1013,70 @@ class ProjectMilestone(GitlabObject): GitlabConnectionError: If the server cannot be reached. GitlabListError: If the server fails to perform the request. """ + # FIXME(gpocentek) url = ('/projects/%s/milestones/%s/merge_requests' % (self.project_id, self.id)) return self.gitlab._raw_list(url, ProjectMergeRequest, **kwargs) -class ProjectMilestoneManager(BaseManager): - obj_cls = ProjectMilestone +class ProjectMilestoneManager(RetrieveMixin, CreateMixin, DeleteMixin, + RESTManager): + _path = '/projects/%(project_id)s/milestones' + _obj_cls = ProjectMilestone + _from_parent_attrs = {'project_id': 'id'} + _create_attrs = (('title', ), ('description', 'due_date', 'start_date', + 'state_event')) + _update_attrs = (tuple(), ('title', 'description', 'due_date', + 'start_date', 'state_event')) + _list_filters = ('iids', 'state') -class ProjectLabel(GitlabObject): - _url = '/projects/%(project_id)s/labels' - _id_in_delete_url = False - _id_in_update_url = False - canGet = 'from_list' - requiredUrlAttrs = ['project_id'] - idAttr = 'name' - requiredDeleteAttrs = ['name'] +class ProjectLabel(SubscribableMixin, SaveMixin, RESTObject): + _id_attr = 'name' requiredCreateAttrs = ['name', 'color'] optionalCreateAttrs = ['description', 'priority'] requiredUpdateAttrs = ['name'] optionalUpdateAttrs = ['new_name', 'color', 'description', 'priority'] - def subscribe(self, **kwargs): - """Subscribe to a label. - - Raises: - GitlabConnectionError: If the server cannot be reached. - GitlabSubscribeError: If the subscription cannot be done - """ - url = ('/projects/%(project_id)s/labels/%(label_id)s/subscribe' % - {'project_id': self.project_id, 'label_id': self.name}) - r = self.gitlab._raw_post(url, **kwargs) - raise_error_from_response(r, GitlabSubscribeError, [201, 304]) - self._set_from_dict(r.json()) +class ProjectLabelManager(GetFromListMixin, CreateMixin, UpdateMixin, + RESTManager): + _path = '/projects/%(project_id)s/labels' + _obj_cls = ProjectLabel + _from_parent_attrs = {'project_id': 'id'} + _create_attrs = (('name', 'color'), ('description', 'priority')) + _update_attrs = (('name', ), + ('new_name', 'color', 'description', 'priority')) - def unsubscribe(self, **kwargs): - """Unsubscribe a label. + # Delete without ID. + def delete(self, name, **kwargs): + """Deletes a Label on the server. - Raises: - GitlabConnectionError: If the server cannot be reached. - GitlabUnsubscribeError: If the unsubscription cannot be done + Args: + name: The name of the label. + **kwargs: Extra data to send to the Gitlab server (e.g. sudo) """ - url = ('/projects/%(project_id)s/labels/%(label_id)s/unsubscribe' % - {'project_id': self.project_id, 'label_id': self.name}) + self.gitlab.http_delete(path, query_data={'name': self.name}, **kwargs) - r = self.gitlab._raw_post(url, **kwargs) - raise_error_from_response(r, GitlabUnsubscribeError, [201, 304]) - self._set_from_dict(r.json()) + # Update without ID, but we need an ID to get from list. + 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) -class ProjectLabelManager(BaseManager): - obj_cls = ProjectLabel + The object is updated to match what the server returns. + """ + updated_data = self._get_updated_data() + # call the manager + server_data = self.manager.update(None, updated_data, **kwargs) + self._update_attrs(server_data) -class ProjectFile(GitlabObject): - _url = '/projects/%(project_id)s/repository/files' - canList = False - requiredUrlAttrs = ['project_id'] - requiredGetAttrs = ['ref'] - requiredCreateAttrs = ['file_path', 'branch', 'content', - 'commit_message'] - optionalCreateAttrs = ['encoding'] - requiredDeleteAttrs = ['branch', 'commit_message', 'file_path'] - shortPrintAttr = 'file_path' + +class ProjectFile(SaveMixin, RESTObject): + _id_attr = 'file_path' + _short_print_attr = 'file_path' def decode(self): """Returns the decoded content of the file. @@ -1436,10 +1087,33 @@ class ProjectFile(GitlabObject): return base64.b64decode(self.content) -class ProjectFileManager(BaseManager): - obj_cls = ProjectFile +class ProjectFileManager(GetMixin, CreateMixin, UpdateMixin, DeleteMixin, + RESTManager): + _path = '/projects/%(project_id)s/repository/files' + _obj_cls = ProjectFile + _from_parent_attrs = {'project_id': 'id'} + _create_attrs = (('file_path', 'branch', 'content', 'commit_message'), + ('encoding', 'author_email', 'author_name')) + _update_attrs = (('file_path', 'branch', 'content', 'commit_message'), + ('encoding', 'author_email', 'author_name')) + + def get(self, file_path, **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. + """ + file_path = file_path.replace('/', '%2F') + return GetMixin.get(self, file_path, **kwargs) - def raw(self, filepath, ref, streamed=False, action=None, chunk_size=1024, + def raw(self, file_path, ref, streamed=False, action=None, chunk_size=1024, **kwargs): """Return the content of a file for a commit. @@ -1460,80 +1134,65 @@ class ProjectFileManager(BaseManager): GitlabConnectionError: If the server cannot be reached. GitlabGetError: If the server fails to perform the request. """ - url = ("/projects/%s/repository/files/%s/raw" % - (self.parent.id, filepath.replace('/', '%2F'))) - url += '?ref=%s' % ref - r = self.gitlab._raw_get(url, streamed=streamed, **kwargs) - raise_error_from_response(r, GitlabGetError) - return utils.response_content(r, streamed, action, chunk_size) - + file_path = file_path.replace('/', '%2F') + path = '%s/%s/raw' % (self.path, file_path) + query_data = {'ref': ref} + result = self.gitlab.http_get(path, query_data=query_data, + streamed=streamed, **kwargs) + return utils.response_content(result, streamed, action, chunk_size) -class ProjectPipeline(GitlabObject): - _url = '/projects/%(project_id)s/pipelines' - _create_url = '/projects/%(project_id)s/pipeline' - canUpdate = False - canDelete = False - - requiredUrlAttrs = ['project_id'] - requiredCreateAttrs = ['ref'] +class ProjectPipeline(RESTObject): + def cancel(self, **kwargs): + """Cancel the job.""" + path = '%s/%s/cancel' % (self.manager.path, self.get_id()) + self.manager.gitlab.http_post(path) def retry(self, **kwargs): - """Retries failed builds in a pipeline. + """Retry the job.""" + path = '%s/%s/retry' % (self.manager.path, self.get_id()) + self.manager.gitlab.http_post(path) - Raises: - GitlabConnectionError: If the server cannot be reached. - GitlabPipelineRetryError: If the retry cannot be done. - """ - url = ('/projects/%(project_id)s/pipelines/%(id)s/retry' % - {'project_id': self.project_id, 'id': self.id}) - r = self.gitlab._raw_post(url, data=None, content_type=None, **kwargs) - raise_error_from_response(r, GitlabPipelineRetryError, 201) - self._set_from_dict(r.json()) - def cancel(self, **kwargs): - """Cancel builds in a pipeline. +class ProjectPipelineManager(RetrieveMixin, CreateMixin, RESTManager): + _path = '/projects/%(project_id)s/pipelines' + _obj_cls = ProjectPipeline + _from_parent_attrs = {'project_id': 'id'} + _create_attrs = (('ref', ), tuple()) - Raises: - GitlabConnectionError: If the server cannot be reached. - GitlabPipelineCancelError: If the retry cannot be done. - """ - url = ('/projects/%(project_id)s/pipelines/%(id)s/cancel' % - {'project_id': self.project_id, 'id': self.id}) - r = self.gitlab._raw_post(url, data=None, content_type=None, **kwargs) - raise_error_from_response(r, GitlabPipelineRetryError, 200) - self._set_from_dict(r.json()) + def create(self, data, **kwargs): + """Creates 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) -class ProjectPipelineManager(BaseManager): - obj_cls = ProjectPipeline + Returns: + RESTObject: a new instance of the manage object class build with + the data sent by the server + """ + path = self.path[:-1] # drop the 's' + return CreateMixin.create(self, data, path=path, **kwargs) -class ProjectSnippetNote(GitlabObject): - _url = '/projects/%(project_id)s/snippets/%(snippet_id)s/notes' - _constructorTypes = {'author': 'User'} - canUpdate = False - canDelete = False - requiredUrlAttrs = ['project_id', 'snippet_id'] - requiredCreateAttrs = ['body'] +class ProjectSnippetNote(RESTObject): + _constructor_types = {'author': 'User'} -class ProjectSnippetNoteManager(BaseManager): - obj_cls = ProjectSnippetNote +class ProjectSnippetNoteManager(RetrieveMixin, CreateMixin, RESTManager): + _path = '/projects/%(project_id)s/snippets/%(snippet_id)s/notes' + _obj_cls = ProjectSnippetNote + _from_parent_attrs = {'project_id': 'project_id', + 'snippet_id': 'id'} + _create_attrs = (('body', ), tuple()) -class ProjectSnippet(GitlabObject): +class ProjectSnippet(SaveMixin, RESTObject): _url = '/projects/%(project_id)s/snippets' - _constructorTypes = {'author': 'User'} - requiredUrlAttrs = ['project_id'] - requiredCreateAttrs = ['title', 'file_name', 'code'] - optionalCreateAttrs = ['lifetime', 'visibility'] - optionalUpdateAttrs = ['title', 'file_name', 'code', 'visibility'] - shortPrintAttr = 'title' - managers = ( - ('notes', 'ProjectSnippetNoteManager', - [('project_id', 'project_id'), ('snippet_id', 'id')]), - ) + _constructor_types = {'author': 'User'} + _short_print_attr = 'title' + _managers = (('notes', 'ProjectSnippetNoteManager'), ) def content(self, streamed=False, action=None, chunk_size=1024, **kwargs): """Return the raw content of a snippet. @@ -1553,23 +1212,22 @@ class ProjectSnippet(GitlabObject): GitlabConnectionError: If the server cannot be reached. GitlabGetError: If the server fails to perform the request. """ - url = ("/projects/%(project_id)s/snippets/%(snippet_id)s/raw" % - {'project_id': self.project_id, 'snippet_id': self.id}) - r = self.gitlab._raw_get(url, **kwargs) - raise_error_from_response(r, GitlabGetError) + path = "%s/%s/raw" % (self.manager.path, self.get_id()) + result = self.manager.gitlab.http_get(path, streamed=streamed, + **kwargs) return utils.response_content(r, streamed, action, chunk_size) -class ProjectSnippetManager(BaseManager): - obj_cls = ProjectSnippet - +class ProjectSnippetManager(CRUDMixin, RESTManager): + _path = '/projects/%(project_id)s/snippets' + _obj_cls = ProjectSnippet + _from_parent_attrs = {'project_id': 'id'} + _create_attrs = (('title', 'file_name', 'code'), + ('lifetime', 'visibility')) + _update_attrs = (tuple(), ('title', 'file_name', 'code', 'visibility')) -class ProjectTrigger(GitlabObject): - _url = '/projects/%(project_id)s/triggers' - requiredUrlAttrs = ['project_id'] - requiredCreateAttrs = ['description'] - optionalUpdateAttrs = ['description'] +class ProjectTrigger(SaveMixin, RESTObject): def take_ownership(self, **kwargs): """Update the owner of a trigger. @@ -1577,26 +1235,29 @@ class ProjectTrigger(GitlabObject): GitlabConnectionError: If the server cannot be reached. GitlabGetError: If the server fails to perform the request. """ - url = ('/projects/%(project_id)s/triggers/%(id)s/take_ownership' % - {'project_id': self.project_id, 'id': self.id}) - r = self.gitlab._raw_post(url, **kwargs) - raise_error_from_response(r, GitlabUpdateError, 200) - self._set_from_dict(r.json()) + path = '%s/%s/take_ownership' % (self.manager.path, self.get_id()) + server_data = self.manager.gitlab.http_post(path, **kwargs) + self._update_attrs(server_data) -class ProjectTriggerManager(BaseManager): - obj_cls = ProjectTrigger +class ProjectTriggerManager(CRUDMixin, RESTManager): + _path = '/projects/%(project_id)s/triggers' + _obj_cls = ProjectTrigger + _from_parent_attrs = {'project_id': 'id'} + _create_attrs = (('description', ), tuple()) + _update_attrs = (('description', ), tuple()) -class ProjectVariable(GitlabObject): - _url = '/projects/%(project_id)s/variables' - idAttr = 'key' - requiredUrlAttrs = ['project_id'] - requiredCreateAttrs = ['key', 'value'] +class ProjectVariable(SaveMixin, RESTObject): + _id_attr = 'key' -class ProjectVariableManager(BaseManager): - obj_cls = ProjectVariable +class ProjectVariableManager(CRUDMixin, RESTManager): + _path = '/projects/%(project_id)s/variables' + _obj_cls = ProjectVariable + _from_parent_attrs = {'project_id': 'id'} + _create_attrs = (('key', 'vaule'), tuple()) + _update_attrs = (('key', 'vaule'), tuple()) class ProjectService(GitlabObject): @@ -1688,113 +1349,70 @@ class ProjectServiceManager(BaseManager): return list(ProjectService._service_attrs.keys()) -class ProjectAccessRequest(GitlabObject): - _url = '/projects/%(project_id)s/access_requests' - canGet = 'from_list' - canUpdate = False +class ProjectAccessRequest(AccessRequestMixin, RESTObject): + pass - def approve(self, access_level=gitlab.DEVELOPER_ACCESS, **kwargs): - """Approve an access request. - Attrs: - access_level (int): The access level for the user. +class ProjectAccessRequestManager(GetFromListMixin, CreateMixin, DeleteMixin, + RESTManager): + _path = '/projects/%(project_id)s/access_requests' + _obj_cls = ProjectAccessRequest + _from_parent_attrs = {'project_id': 'id'} - Raises: - GitlabConnectionError: If the server cannot be reached. - GitlabUpdateError: If the server fails to perform the request. - """ - url = ('/projects/%(project_id)s/access_requests/%(id)s/approve' % - {'project_id': self.project_id, 'id': self.id}) - data = {'access_level': access_level} - r = self.gitlab._raw_put(url, data=data, **kwargs) - raise_error_from_response(r, GitlabUpdateError, 201) - self._set_from_dict(r.json()) +class ProjectDeployment(RESTObject): + pass -class ProjectAccessRequestManager(BaseManager): - obj_cls = ProjectAccessRequest +class ProjectDeploymentManager(RetrieveMixin, RESTManager): + _path = '/projects/%(project_id)s/deployments' + _obj_cls = ProjectDeployment + _from_parent_attrs = {'project_id': 'id'} -class ProjectDeployment(GitlabObject): - _url = '/projects/%(project_id)s/deployments' - canCreate = False +class ProjectRunner(RESTObject): canUpdate = False - canDelete = False + requiredCreateAttrs = ['runner_id'] -class ProjectDeploymentManager(BaseManager): - obj_cls = ProjectDeployment +class ProjectRunnerManager(NoUpdateMixin, RESTManager): + _path = '/projects/%(project_id)s/runners' + _obj_cls = ProjectRunner + _from_parent_attrs = {'project_id': 'id'} + _create_attrs = (('runner_id', ), tuple()) -class ProjectRunner(GitlabObject): - _url = '/projects/%(project_id)s/runners' - canUpdate = False - requiredCreateAttrs = ['runner_id'] - -class ProjectRunnerManager(BaseManager): - obj_cls = ProjectRunner - - -class Project(GitlabObject): - _url = '/projects' - _constructorTypes = {'owner': 'User', 'namespace': 'Group'} - optionalListAttrs = ['search'] - requiredCreateAttrs = ['name'] - optionalListAttrs = ['search', 'owned', 'starred', 'archived', - 'visibility', 'order_by', 'sort', 'simple', - 'membership', 'statistics'] - optionalCreateAttrs = ['path', 'namespace_id', 'description', - 'issues_enabled', 'merge_requests_enabled', - 'builds_enabled', 'wiki_enabled', - 'snippets_enabled', 'container_registry_enabled', - 'shared_runners_enabled', 'visibility', - 'import_url', 'public_builds', - 'only_allow_merge_if_build_succeeds', - 'only_allow_merge_if_all_discussions_are_resolved', - 'lfs_enabled', 'request_access_enabled'] - optionalUpdateAttrs = ['name', 'path', 'default_branch', 'description', - 'issues_enabled', 'merge_requests_enabled', - 'builds_enabled', 'wiki_enabled', - 'snippets_enabled', 'container_registry_enabled', - 'shared_runners_enabled', 'visibility', - 'import_url', 'public_builds', - 'only_allow_merge_if_build_succeeds', - 'only_allow_merge_if_all_discussions_are_resolved', - 'lfs_enabled', 'request_access_enabled'] - shortPrintAttr = 'path' - managers = ( - ('accessrequests', 'ProjectAccessRequestManager', - [('project_id', 'id')]), - ('boards', 'ProjectBoardManager', [('project_id', 'id')]), - ('board_lists', 'ProjectBoardListManager', [('project_id', 'id')]), - ('branches', 'ProjectBranchManager', [('project_id', 'id')]), - ('jobs', 'ProjectJobManager', [('project_id', 'id')]), - ('commits', 'ProjectCommitManager', [('project_id', 'id')]), - ('deployments', 'ProjectDeploymentManager', [('project_id', 'id')]), - ('environments', 'ProjectEnvironmentManager', [('project_id', 'id')]), - ('events', 'ProjectEventManager', [('project_id', 'id')]), - ('files', 'ProjectFileManager', [('project_id', 'id')]), - ('forks', 'ProjectForkManager', [('project_id', 'id')]), - ('hooks', 'ProjectHookManager', [('project_id', 'id')]), - ('keys', 'ProjectKeyManager', [('project_id', 'id')]), - ('issues', 'ProjectIssueManager', [('project_id', 'id')]), - ('labels', 'ProjectLabelManager', [('project_id', 'id')]), - ('members', 'ProjectMemberManager', [('project_id', 'id')]), - ('mergerequests', 'ProjectMergeRequestManager', - [('project_id', 'id')]), - ('milestones', 'ProjectMilestoneManager', [('project_id', 'id')]), - ('notes', 'ProjectNoteManager', [('project_id', 'id')]), - ('notificationsettings', 'ProjectNotificationSettingsManager', - [('project_id', 'id')]), - ('pipelines', 'ProjectPipelineManager', [('project_id', 'id')]), - ('runners', 'ProjectRunnerManager', [('project_id', 'id')]), - ('services', 'ProjectServiceManager', [('project_id', 'id')]), - ('snippets', 'ProjectSnippetManager', [('project_id', 'id')]), - ('tags', 'ProjectTagManager', [('project_id', 'id')]), - ('triggers', 'ProjectTriggerManager', [('project_id', 'id')]), - ('variables', 'ProjectVariableManager', [('project_id', 'id')]), +class Project(SaveMixin, RESTObject): + _constructor_types = {'owner': 'User', 'namespace': 'Group'} + _short_print_attr = 'path' + _managers = ( + ('accessrequests', 'ProjectAccessRequestManager'), + ('boards', 'ProjectBoardManager'), + ('branches', 'ProjectBranchManager'), + ('jobs', 'ProjectJobManager'), + ('commits', 'ProjectCommitManager'), + ('deployments', 'ProjectDeploymentManager'), + ('environments', 'ProjectEnvironmentManager'), + ('events', 'ProjectEventManager'), + ('files', 'ProjectFileManager'), + ('forks', 'ProjectForkManager'), + ('hooks', 'ProjectHookManager'), + ('keys', 'ProjectKeyManager'), + ('issues', 'ProjectIssueManager'), + ('labels', 'ProjectLabelManager'), + ('members', 'ProjectMemberManager'), + ('mergerequests', 'ProjectMergeRequestManager'), + ('milestones', 'ProjectMilestoneManager'), + ('notes', 'ProjectNoteManager'), + ('notificationsettings', 'ProjectNotificationSettingsManager'), + ('pipelines', 'ProjectPipelineManager'), + ('runners', 'ProjectRunnerManager'), + ('services', 'ProjectServiceManager'), + ('snippets', 'ProjectSnippetManager'), + ('tags', 'ProjectTagManager'), + ('triggers', 'ProjectTriggerManager'), + ('variables', 'ProjectVariableManager'), ) def repository_tree(self, path='', ref='', **kwargs): @@ -1811,17 +1429,14 @@ class Project(GitlabObject): GitlabConnectionError: If the server cannot be reached. GitlabGetError: If the server fails to perform the request. """ - url = "/projects/%s/repository/tree" % (self.id) - params = [] + path = '/projects/%s/repository/tree' % self.get_id() + query_data = {} if path: - params.append(urllib.parse.urlencode({'path': path})) + query_data['path'] = path if ref: - params.append("ref=%s" % ref) - if params: - url += '?' + "&".join(params) - r = self.gitlab._raw_get(url, **kwargs) - raise_error_from_response(r, GitlabGetError) - return r.json() + query_data['ref'] = ref + return self.manager.gitlab.http_get(path, query_data=query_data, + **kwargs) def repository_raw_blob(self, sha, streamed=False, action=None, chunk_size=1024, **kwargs): @@ -1843,10 +1458,9 @@ class Project(GitlabObject): GitlabConnectionError: If the server cannot be reached. GitlabGetError: If the server fails to perform the request. """ - url = "/projects/%s/repository/raw_blobs/%s" % (self.id, sha) - r = self.gitlab._raw_get(url, streamed=streamed, **kwargs) - raise_error_from_response(r, GitlabGetError) - return utils.response_content(r, streamed, action, chunk_size) + path = '/projects/%s/repository/raw_blobs/%s' % (self.get_id(), sha) + result = self.gitlab._raw_get(url, streamed=streamed, **kwargs) + return utils.response_content(result, streamed, action, chunk_size) def repository_compare(self, from_, to, **kwargs): """Returns a diff between two branches/commits. @@ -1862,13 +1476,12 @@ class Project(GitlabObject): GitlabConnectionError: If the server cannot be reached. GitlabGetError: If the server fails to perform the request. """ - url = "/projects/%s/repository/compare" % self.id - url = "%s?from=%s&to=%s" % (url, from_, to) - r = self.gitlab._raw_get(url, **kwargs) - raise_error_from_response(r, GitlabGetError) - return r.json() + path = '/projects/%s/repository/compare' % self.get_id() + query_data = {'from': from_, 'to': to} + return self.manager.gitlab.http_get(path, query_data=query_data, + **kwargs) - def repository_contributors(self): + def repository_contributors(self, **kwargs): """Returns a list of contributors for the project. Returns: @@ -1878,10 +1491,8 @@ class Project(GitlabObject): GitlabConnectionError: If the server cannot be reached. GitlabGetError: If the server fails to perform the request. """ - url = "/projects/%s/repository/contributors" % self.id - r = self.gitlab._raw_get(url) - raise_error_from_response(r, GitlabListError) - return r.json() + path = '/projects/%s/repository/contributors' % self.get_id() + return self.manager.gitlab.http_get(path, **kwargs) def repository_archive(self, sha=None, streamed=False, action=None, chunk_size=1024, **kwargs): @@ -1903,14 +1514,15 @@ class Project(GitlabObject): GitlabConnectionError: If the server cannot be reached. GitlabGetError: If the server fails to perform the request. """ - url = '/projects/%s/repository/archive' % self.id + path = '/projects/%s/repository/archive' % self.get_id() + query_data = {} if sha: - url += '?sha=%s' % sha - r = self.gitlab._raw_get(url, streamed=streamed, **kwargs) - raise_error_from_response(r, GitlabGetError) - return utils.response_content(r, streamed, action, chunk_size) + query_data['sha'] = sha + result = self.gitlab._raw_get(path, query_data=query_data, + streamed=streamed, **kwargs) + return utils.response_content(result, streamed, action, chunk_size) - def create_fork_relation(self, forked_from_id): + def create_fork_relation(self, forked_from_id, **kwargs): """Create a forked from/to relation between existing projects. Args: @@ -1920,20 +1532,18 @@ class Project(GitlabObject): GitlabConnectionError: If the server cannot be reached. GitlabCreateError: If the server fails to perform the request. """ - url = "/projects/%s/fork/%s" % (self.id, forked_from_id) - r = self.gitlab._raw_post(url) - raise_error_from_response(r, GitlabCreateError, 201) + path = '/projects/%s/fork/%s' % (self.get_id(), forked_from_id) + self.manager.gitlab.http_post(path, **kwargs) - def delete_fork_relation(self): + def delete_fork_relation(self, **kwargs): """Delete a forked relation between existing projects. Raises: GitlabConnectionError: If the server cannot be reached. GitlabDeleteError: If the server fails to perform the request. """ - url = "/projects/%s/fork" % self.id - r = self.gitlab._raw_delete(url) - raise_error_from_response(r, GitlabDeleteError) + path = '/projects/%s/fork' % self.get_id() + self.manager.gitlab.http_delete(path, **kwargs) def star(self, **kwargs): """Star a project. @@ -1945,10 +1555,9 @@ class Project(GitlabObject): GitlabCreateError: If the action cannot be done GitlabConnectionError: If the server cannot be reached. """ - url = "/projects/%s/star" % self.id - r = self.gitlab._raw_post(url, **kwargs) - raise_error_from_response(r, GitlabCreateError, [201, 304]) - return Project(self.gitlab, r.json()) if r.status_code == 201 else self + path = '/projects/%s/star' % self.get_id() + server_data = self.manager.gitlab.http_post(path, **kwargs) + self._update_attrs(server_data) def unstar(self, **kwargs): """Unstar a project. @@ -1960,10 +1569,9 @@ class Project(GitlabObject): GitlabDeleteError: If the action cannot be done GitlabConnectionError: If the server cannot be reached. """ - url = "/projects/%s/unstar" % self.id - r = self.gitlab._raw_post(url, **kwargs) - raise_error_from_response(r, GitlabDeleteError, [201, 304]) - return Project(self.gitlab, r.json()) if r.status_code == 201 else self + path = '/projects/%s/unstar' % self.get_id() + server_data = self.manager.gitlab.http_post(path, **kwargs) + self._update_attrs(server_data) def archive(self, **kwargs): """Archive a project. @@ -1975,10 +1583,9 @@ class Project(GitlabObject): GitlabCreateError: If the action cannot be done GitlabConnectionError: If the server cannot be reached. """ - url = "/projects/%s/archive" % self.id - r = self.gitlab._raw_post(url, **kwargs) - raise_error_from_response(r, GitlabCreateError, 201) - return Project(self.gitlab, r.json()) if r.status_code == 201 else self + path = '/projects/%s/archive' % self.get_id() + server_data = self.manager.gitlab.http_post(path, **kwargs) + self._update_attrs(server_data) def unarchive(self, **kwargs): """Unarchive a project. @@ -1990,12 +1597,11 @@ class Project(GitlabObject): GitlabDeleteError: If the action cannot be done GitlabConnectionError: If the server cannot be reached. """ - url = "/projects/%s/unarchive" % self.id - r = self.gitlab._raw_delete(url, **kwargs) - raise_error_from_response(r, GitlabCreateError, 201) - return Project(self.gitlab, r.json()) if r.status_code == 201 else self + path = '/projects/%s/unarchive' % self.get_id() + server_data = self.manager.gitlab.http_post(url, **kwargs) + self._update_attrs(server_data) - def share(self, group_id, group_access, **kwargs): + def share(self, group_id, group_access, expires_at=None, **kwargs): """Share the project with a group. Args: @@ -2006,10 +1612,11 @@ class Project(GitlabObject): GitlabConnectionError: If the server cannot be reached. GitlabCreateError: If the server fails to perform the request. """ - url = "/projects/%s/share" % self.id - data = {'group_id': group_id, 'group_access': group_access} - r = self.gitlab._raw_post(url, data=data, **kwargs) - raise_error_from_response(r, GitlabCreateError, 201) + path = '/projects/%s/share' % self.get_id() + data = {'group_id': group_id, + 'group_access': group_access, + 'expires_at': expires_at} + self.manager.gitlab.http_post(path, post_data=data, **kwargs) def trigger_pipeline(self, ref, token, variables={}, **kwargs): """Trigger a CI build. @@ -2025,23 +1632,23 @@ class Project(GitlabObject): GitlabConnectionError: If the server cannot be reached. GitlabCreateError: If the server fails to perform the request. """ - url = "/projects/%s/trigger/pipeline" % self.id + path = '/projects/%s/trigger/pipeline' % self.get_id() form = {r'variables[%s]' % k: v for k, v in six.iteritems(variables)} - data = {'ref': ref, 'token': token} - data.update(form) - r = self.gitlab._raw_post(url, data=data, **kwargs) - raise_error_from_response(r, GitlabCreateError, 201) + post_data = {'ref': ref, 'token': token} + post_data.update(form) + self.manager.gitlab.http_post(path, post_data=post_data, **kwargs) -class Runner(GitlabObject): - _url = '/runners' - canCreate = False - optionalUpdateAttrs = ['description', 'active', 'tag_list'] - optionalListAttrs = ['scope'] +class Runner(SaveMixin, RESTObject): + pass -class RunnerManager(BaseManager): - obj_cls = Runner +class RunnerManager(RetrieveMixin, UpdateMixin, DeleteMixin, RESTManager): + _path = '/runners' + _obj_cls = Runner + _update_attrs = (tuple(), ('description', 'active', 'tag_list')) + _list_filters = ('scope', ) + def all(self, scope=None, **kwargs): """List all the runners. @@ -2057,79 +1664,95 @@ class RunnerManager(BaseManager): GitlabConnectionError: If the server cannot be reached. GitlabListError: If the resource cannot be found """ - url = '/runners/all' + path = '/runners/all' + query_data = {} if scope is not None: - url += '?scope=' + scope - return self.gitlab._raw_list(url, self.obj_cls, **kwargs) + query_data['scope'] = scope + return self.gitlab.http_list(path, query_data, **kwargs) -class Todo(GitlabObject): - _url = '/todos' - canGet = 'from_list' - canUpdate = False - canCreate = False - optionalListAttrs = ['action', 'author_id', 'project_id', 'state', 'type'] +class Todo(RESTObject): + def mark_as_done(self, **kwargs): + """Mark the todo as done. + + Args: + **kwargs: Additional data to send to the server (e.g. sudo) + """ + path = '%s/%s/mark_as_done' % (self.manager.path, self.id) + server_data = self.manager.gitlab.http_post(path, **kwargs) + self._update_attrs(server_data) -class TodoManager(BaseManager): - obj_cls = Todo +class TodoManager(GetFromListMixin, DeleteMixin, RESTManager): + _path = '/todos' + _obj_cls = Todo + _list_filters = ('action', 'author_id', 'project_id', 'state', 'type') - def delete_all(self, **kwargs): + def mark_all_as_done(self, **kwargs): """Mark all the todos as done. + Returns: + The number of todos maked done. + Raises: GitlabConnectionError: If the server cannot be reached. GitlabDeleteError: If the resource cannot be found - - Returns: - The number of todos maked done. """ - url = '/todos' - r = self.gitlab._raw_delete(url, **kwargs) - raise_error_from_response(r, GitlabDeleteError) - return int(r.text) - - -class ProjectManager(BaseManager): - obj_cls = Project - + self.gitlab.http_post('/todos/mark_as_done', **kwargs) + + +class ProjectManager(CRUDMixin, RESTManager): + _path = '/projects' + _obj_cls = Project + _create_attrs = ( + ('name', ), + ('path', 'namespace_id', 'description', 'issues_enabled', + 'merge_requests_enabled', 'builds_enabled', 'wiki_enabled', + 'snippets_enabled', 'container_registry_enabled', + 'shared_runners_enabled', 'visibility', 'import_url', 'public_builds', + 'only_allow_merge_if_build_succeeds', + 'only_allow_merge_if_all_discussions_are_resolved', 'lfs_enabled', + 'request_access_enabled') + ) + _update_attrs = ( + tuple(), + ('name', 'path', 'default_branch', 'description', 'issues_enabled', + 'merge_requests_enabled', 'builds_enabled', 'wiki_enabled', + 'snippets_enabled', 'container_registry_enabled', + 'shared_runners_enabled', 'visibility', 'import_url', 'public_builds', + 'only_allow_merge_if_build_succeeds', + 'only_allow_merge_if_all_discussions_are_resolved', 'lfs_enabled', + 'request_access_enabled') + ) + _list_filters = ('search', 'owned', 'starred', 'archived', 'visibility', + 'order_by', 'sort', 'simple', 'membership', 'statistics') -class GroupProject(Project): - _url = '/groups/%(group_id)s/projects' - canGet = 'from_list' - canCreate = False - canDelete = False - canUpdate = False - optionalListAttrs = ['archived', 'visibility', 'order_by', 'sort', - 'search', 'ci_enabled_first'] +class GroupProject(RESTObject): def __init__(self, *args, **kwargs): Project.__init__(self, *args, **kwargs) -class GroupProjectManager(ProjectManager): - obj_cls = GroupProject - - -class Group(GitlabObject): - _url = '/groups' - requiredCreateAttrs = ['name', 'path'] - optionalCreateAttrs = ['description', 'visibility', 'parent_id', - 'lfs_enabled', 'request_access_enabled'] - optionalUpdateAttrs = ['name', 'path', 'description', 'visibility', - 'lfs_enabled', 'request_access_enabled'] - shortPrintAttr = 'name' - managers = ( - ('accessrequests', 'GroupAccessRequestManager', [('group_id', 'id')]), - ('members', 'GroupMemberManager', [('group_id', 'id')]), - ('notificationsettings', 'GroupNotificationSettingsManager', - [('group_id', 'id')]), - ('projects', 'GroupProjectManager', [('group_id', 'id')]), - ('issues', 'GroupIssueManager', [('group_id', 'id')]), +class GroupProjectManager(GetFromListMixin, RESTManager): + _path = '/groups/%(group_id)s/projects' + _obj_cls = GroupProject + _from_parent_attrs = {'group_id': 'id'} + _list_filters = ('archived', 'visibility', 'order_by', 'sort', 'search', + 'ci_enabled_first') + + +class Group(SaveMixin, RESTObject): + _short_print_attr = 'name' + _managers = ( + ('accessrequests', 'GroupAccessRequestManager'), + ('members', 'GroupMemberManager'), + ('notificationsettings', 'GroupNotificationSettingsManager'), + ('projects', 'GroupProjectManager'), + ('issues', 'GroupIssueManager'), ) def transfer_project(self, id, **kwargs): - """Transfers a project to this new groups. + """Transfers a project to this group. Attrs: id (int): ID of the project to transfer. @@ -2139,10 +1762,20 @@ class Group(GitlabObject): GitlabTransferProjectError: If the server fails to perform the request. """ - url = '/groups/%d/projects/%d' % (self.id, id) - r = self.gitlab._raw_post(url, None, **kwargs) - raise_error_from_response(r, GitlabTransferProjectError, 201) + path = '/groups/%d/projects/%d' % (self.id, id) + self.manager.gitlab.http_post(path, **kwargs) -class GroupManager(BaseManager): - obj_cls = Group +class GroupManager(CRUDMixin, RESTManager): + _path = '/groups' + _obj_cls = Group + _create_attrs = ( + ('name', 'path'), + ('description', 'visibility', 'parent_id', 'lfs_enabled', + 'request_access_enabled') + ) + _update_attrs = ( + tuple(), + ('name', 'path', 'description', 'visibility', 'lfs_enabled', + 'request_access_enabled') + ) |