diff --git a/AUTHORS b/AUTHORS index 3e38faff0..d01d5783e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -8,7 +8,10 @@ Contributors ------------ Adam Reid +Alex Widener Amar Sood (tekacs) +Andjelko Horvat +Andreas Nüßlein Andrew Austin Armin Weihbold Asher256 @@ -28,6 +31,7 @@ hakkeroid itxaka Ivica Arsov James (d0c_s4vage) Johnson +James Johnson Jason Antman Jonathon Reinhart Koen Smets diff --git a/ChangeLog b/ChangeLog index e769d163f..14415f9ac 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,18 @@ +Version 0.19 + + * Update project.archive() docs + * Support the scope attribute in runners.list() + * Add support for project runners + * Add support for commit creation + * Fix install doc + * Add builds-email and pipelines-email services + * Deploy keys: rework enable/disable + * Document the dynamic aspect of objects + * Add pipeline_events to ProjectHook attrs + * Add due_date attribute to ProjectIssue + * Handle settings.domain_whitelist, partly + * {Project,Group}Member: support expires_at attribute + Version 0.18 * Fix JIRA service editing for GitLab 8.14+ diff --git a/docs/api-usage.rst b/docs/api-usage.rst index 4f8cb3717..a15aecbfa 100644 --- a/docs/api-usage.rst +++ b/docs/api-usage.rst @@ -2,7 +2,7 @@ Getting started with the API ############################ -The ``gitlab`` package provides 3 basic types: +The ``gitlab`` package provides 3 base types: * ``gitlab.Gitlab`` is the primary class, handling the HTTP requests. It holds the GitLab URL and authentication information. @@ -68,6 +68,17 @@ Examples: user = gl.users.create(user_data) print(user) +The attributes of objects are defined upon object creation, and depend on the +GitLab API itself. To list the available information associated with an object +use the python introspection tools: + +.. code-block:: python + + project = gl.projects.get(1) + print(vars(project)) + # or + print(project.__dict__) + Some ``gitlab.GitlabObject`` classes also provide managers to access related GitLab resources: diff --git a/docs/gl_objects/commits.py b/docs/gl_objects/commits.py index 30465139e..2ed66f560 100644 --- a/docs/gl_objects/commits.py +++ b/docs/gl_objects/commits.py @@ -9,6 +9,26 @@ commits = project.commits.list(since='2016-01-01T00:00:00Z') # end filter list +# create +# See https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions +# for actions detail +data = { + 'branch_name': 'master', + 'commit_message': 'blah blah blah', + 'actions': [ + { + 'action': 'create', + 'file_path': 'blah', + 'content': 'blah' + } + ] +} + +commit = gl.project_commits.create(data, project_id=1) +# or +commit = project.commits.create(data) +# end commit + # get commit = gl.project_commits.get('e3d5a71b', project_id=1) # or diff --git a/docs/gl_objects/commits.rst b/docs/gl_objects/commits.rst index 5a43597a5..8be1b8602 100644 --- a/docs/gl_objects/commits.rst +++ b/docs/gl_objects/commits.rst @@ -5,10 +5,9 @@ Commits Commits ======= -Use :class:`~gitlab.objects.ProjectCommit` objects to manipulate commits. The -:attr:`gitlab.Gitlab.project_commits` and -:attr:`gitlab.objects.Project.commits` manager objects provide helper -functions. +* Object class: :class:`~gitlab.objects.ProjectCommit` +* Manager objects: :attr:`gitlab.Gitlab.project_commits`, + :attr:`gitlab.objects.Project.commits` Examples -------- @@ -26,6 +25,12 @@ results: :start-after: # filter list :end-before: # end filter list +Create a commit: + +.. literalinclude:: commits.py + :start-after: # create + :end-before: # end create + Get a commit detail: .. literalinclude:: commits.py @@ -41,11 +46,10 @@ Get the diff for a commit: Commit comments =============== -Use :class:`~gitlab.objects.ProjectCommitStatus` objects to manipulate commits. The -:attr:`gitlab.Gitlab.project_commit_comments` and -:attr:`gitlab.objects.Project.commit_comments` and -:attr:`gitlab.objects.ProjectCommit.comments` manager objects provide helper -functions. +* Object class: :class:`~gitlab.objects.ProjectCommiComment` +* Manager objects: :attr:`gitlab.Gitlab.project_commit_comments`, + :attr:`gitlab.objects.Project.commit_comments`, + :attr:`gitlab.objects.ProjectCommit.comments` Examples -------- @@ -65,11 +69,10 @@ Add a comment on a commit: Commit status ============= -Use :class:`~gitlab.objects.ProjectCommitStatus` objects to manipulate commits. -The :attr:`gitlab.Gitlab.project_commit_statuses`, -:attr:`gitlab.objects.Project.commit_statuses` and -:attr:`gitlab.objects.ProjectCommit.statuses` manager objects provide helper -functions. +* Object class: :class:`~gitlab.objects.ProjectCommitStatus` +* Manager objects: :attr:`gitlab.Gitlab.project_commit_statuses`, + :attr:`gitlab.objects.Project.commit_statuses`, + :attr:`gitlab.objects.ProjectCommit.statuses` Examples -------- diff --git a/docs/gl_objects/deploy_keys.py b/docs/gl_objects/deploy_keys.py index f144d9ef9..5d85055a7 100644 --- a/docs/gl_objects/deploy_keys.py +++ b/docs/gl_objects/deploy_keys.py @@ -4,7 +4,7 @@ # global get key = gl.keys.get(key_id) -# end global key +# end global get # list keys = gl.project_keys.list(project_id=1) @@ -36,9 +36,9 @@ # end delete # enable -deploy_key.enable() +project.keys.enable(key_id) # end enable # disable -deploy_key.disable() +project.keys.disable(key_id) # end disable diff --git a/docs/gl_objects/projects.py b/docs/gl_objects/projects.py index ed99cec44..54bde842e 100644 --- a/docs/gl_objects/projects.py +++ b/docs/gl_objects/projects.py @@ -67,8 +67,8 @@ # end star # archive -project.archive_() -project.unarchive_() +project.archive() +project.unarchive() # end archive # events list diff --git a/docs/gl_objects/projects.rst b/docs/gl_objects/projects.rst index 584fa58f6..dc6c48baf 100644 --- a/docs/gl_objects/projects.rst +++ b/docs/gl_objects/projects.rst @@ -94,9 +94,8 @@ Archive/unarchive a project: .. note:: - The underscore character at the end of the methods is used to workaround a - conflict with a previous misuse of the ``archive`` method (deprecated but - not yet removed). + Previous versions used ``archive_`` and ``unarchive_`` due to a naming issue, + they have been deprecated but not yet removed. Repository ---------- diff --git a/docs/gl_objects/runners.py b/docs/gl_objects/runners.py index 5092dc08f..1a9cb82dd 100644 --- a/docs/gl_objects/runners.py +++ b/docs/gl_objects/runners.py @@ -1,6 +1,8 @@ # list # List owned runners runners = gl.runners.list() +# With a filter +runners = gl.runners.list(scope='active') # List all runners, using a filter runners = gl.runners.all(scope='paused') # end list @@ -20,3 +22,21 @@ # or runner.delete() # end delete + +# project list +runners = gl.project_runners.list(project_id=1) +# or +runners = project.runners.list() +# end project list + +# project enable +p_runner = gl.project_runners.create({'runner_id': runner.id}, project_id=1) +# or +p_runner = project.runners.create({'runner_id': runner.id}) +# end project enable + +# project disable +gl.project_runners.delete(runner.id) +# or +project.runners.delete(runner.id) +# end project disable diff --git a/docs/gl_objects/runners.rst b/docs/gl_objects/runners.rst index 32d671999..02db9be3a 100644 --- a/docs/gl_objects/runners.rst +++ b/docs/gl_objects/runners.rst @@ -2,20 +2,31 @@ Runners ####### -Global runners -============== +Runners are external process used to run CI jobs. They are deployed by the +administrator and registered to the GitLab instance. -Use :class:`~gitlab.objects.Runner` objects to manipulate runners. The -:attr:`gitlab.Gitlab.runners` manager object provides helper functions. +Shared runners are available for all projects. Specific runners are enabled for +a list of projects. + +Global runners (admin) +====================== + +* Object class: :class:`~gitlab.objects.Runner` +* Manager objects: :attr:`gitlab.Gitlab.runners` Examples -------- Use the ``list()`` and ``all()`` methods to list runners. -The ``all()`` method accepts a ``scope`` parameter to filter the list. Allowed -values for this parameter are ``specific``, ``shared``, ``active``, ``paused`` -and ``online``. +Both methods accept a ``scope`` parameter to filter the list. Allowed values +for this parameter are: + +* ``active`` +* ``paused`` +* ``online`` +* ``specific`` (``all()`` only) +* ``shared`` (``all()`` only) .. note:: @@ -43,3 +54,31 @@ Remove a runner: .. literalinclude:: runners.py :start-after: # delete :end-before: # end delete + +Project runners +=============== + +* Object class: :class:`~gitlab.objects.ProjectRunner` +* Manager objects: :attr:`gitlab.Gitlab.runners`, + :attr:`gitlab.Gitlab.Project.runners` + +Examples +-------- + +List the runners for a project: + +.. literalinclude:: runners.py + :start-after: # project list + :end-before: # end project list + +Enable a specific runner for a project: + +.. literalinclude:: runners.py + :start-after: # project enable + :end-before: # end project enable + +Disable a specific runner for a project: + +.. literalinclude:: runners.py + :start-after: # project disable + :end-before: # end project disable diff --git a/docs/install.rst b/docs/install.rst index 6abba3f03..fc9520400 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -12,7 +12,7 @@ Use :command:`pip` to install the latest stable version of ``python-gitlab``: The current development version is available on `github `__. Use :command:`git` and -:command:`pip` to install it: +:command:`python setup.py` to install it: .. code-block:: console diff --git a/gitlab/__init__.py b/gitlab/__init__.py index e0051aafd..119dab080 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -34,11 +34,11 @@ from gitlab.objects import * # noqa __title__ = 'python-gitlab' -__version__ = '0.18' +__version__ = '0.19' __author__ = 'Gauvain Pocentek' __email__ = 'gauvain@pocentek.net' __license__ = 'LGPL3' -__copyright__ = 'Copyright 2013-2016 Gauvain Pocentek' +__copyright__ = 'Copyright 2013-2017 Gauvain Pocentek' warnings.filterwarnings('default', category=DeprecationWarning, module='^gitlab') diff --git a/gitlab/objects.py b/gitlab/objects.py index 2a33dc518..ea40b6f7d 100644 --- a/gitlab/objects.py +++ b/gitlab/objects.py @@ -99,6 +99,8 @@ def get(self, id=None, **kwargs): args = self._set_parent_args(**kwargs) if not self.obj_cls.canGet: raise NotImplementedError + if id is None and self.obj_cls.getRequiresId is True: + raise ValueError('The id argument must be defined.') return self.obj_cls.get(self.gitlab, id, **args) def list(self, **kwargs): @@ -220,9 +222,10 @@ def _data_for_gitlab(self, extra_parameters={}, update=False, for attribute in attributes: if hasattr(self, attribute): value = getattr(self, attribute) - if isinstance(value, list): - value = ",".join(value) - if attribute == 'sudo': + # labels need to be sent as a comma-separated list + if attribute == 'labels' and isinstance(value, list): + value = ", ".join(value) + elif attribute == 'sudo': value = str(value) data[attribute] = value @@ -734,6 +737,7 @@ class CurrentUser(GitlabObject): class ApplicationSettings(GitlabObject): _url = '/application/settings' _id_in_update_url = False + getRequiresId = False optionalUpdateAttrs = ['after_sign_out_path', 'container_registry_token_expire_delay', 'default_branch_protection', @@ -760,6 +764,15 @@ class ApplicationSettings(GitlabObject): canCreate = False canDelete = False + def _data_for_gitlab(self, extra_parameters={}, update=False, + as_json=True): + data = (super(ApplicationSettings, self) + ._data_for_gitlab(extra_parameters, update=update, + as_json=False)) + if not self.domain_whitelist: + data.pop('domain_whitelist', None) + return json.dumps(data) + class ApplicationSettingsManager(BaseManager): obj_cls = ApplicationSettings @@ -792,6 +805,7 @@ class KeyManager(BaseManager): class NotificationSettings(GitlabObject): _url = '/notification_settings' _id_in_update_url = False + getRequiresId = False optionalUpdateAttrs = ['level', 'notification_email', 'new_note', @@ -856,7 +870,9 @@ class GroupMember(GitlabObject): 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): @@ -1274,8 +1290,9 @@ class ProjectCommit(GitlabObject): _url = '/projects/%(project_id)s/repository/commits' canDelete = False canUpdate = False - canCreate = False requiredUrlAttrs = ['project_id'] + requiredCreateAttrs = ['branch_name', 'commit_message', 'actions'] + optionalCreateAttrs = ['author_email', 'author_name'] shortPrintAttr = 'title' managers = ( ('comments', ProjectCommitCommentManager, @@ -1360,24 +1377,23 @@ class ProjectKey(GitlabObject): requiredUrlAttrs = ['project_id'] requiredCreateAttrs = ['title', 'key'] - def enable(self): + +class ProjectKeyManager(BaseManager): + obj_cls = ProjectKey + + def enable(self, key_id): """Enable a deploy key for a project.""" - url = '/projects/%s/deploy_keys/%s/enable' % (self.project_id, self.id) + 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) - def disable(self): + def disable(self, key_id): """Disable a deploy key for a project.""" - url = '/projects/%s/deploy_keys/%s/disable' % (self.project_id, - self.id) + url = '/projects/%s/deploy_keys/%s/disable' % (self.parent.id, key_id) r = self.gitlab._raw_delete(url) raise_error_from_response(r, GitlabProjectDeployKeyError, 200) -class ProjectKeyManager(BaseManager): - obj_cls = ProjectKey - - class ProjectEvent(GitlabObject): _url = '/projects/%(project_id)s/events' canGet = 'from_list' @@ -1412,7 +1428,8 @@ class ProjectHook(GitlabObject): requiredCreateAttrs = ['url'] optionalCreateAttrs = ['push_events', 'issues_events', 'note_events', 'merge_requests_events', 'tag_push_events', - 'build_events', 'enable_ssl_verification', 'token'] + 'build_events', 'enable_ssl_verification', 'token', + 'pipeline_events'] shortPrintAttr = 'url' @@ -1442,29 +1459,16 @@ class ProjectIssue(GitlabObject): requiredUrlAttrs = ['project_id'] requiredCreateAttrs = ['title'] optionalCreateAttrs = ['description', 'assignee_id', 'milestone_id', - 'labels', 'created_at'] + 'labels', 'created_at', 'due_date'] optionalUpdateAttrs = ['title', 'description', 'assignee_id', 'milestone_id', 'labels', 'created_at', - 'updated_at', 'state_event'] + 'updated_at', 'state_event', 'due_date'] shortPrintAttr = 'title' managers = ( ('notes', ProjectIssueNoteManager, [('project_id', 'project_id'), ('issue_id', 'id')]), ) - def _data_for_gitlab(self, extra_parameters={}, update=False, - as_json=True): - # Gitlab-api returns labels in a json list and takes them in a - # comma separated list. - if hasattr(self, "labels"): - if (self.labels is not None and - not isinstance(self.labels, six.string_types)): - labels = ", ".join(self.labels) - extra_parameters['labels'] = labels - - return super(ProjectIssue, self)._data_for_gitlab(extra_parameters, - update) - def subscribe(self, **kwargs): """Subscribe to an issue. @@ -1528,7 +1532,9 @@ class ProjectMember(GitlabObject): _url = '/projects/%(project_id)s/members' requiredUrlAttrs = ['project_id'] requiredCreateAttrs = ['access_level', 'user_id'] + optionalCreateAttrs = ['expires_at'] requiredUpdateAttrs = ['access_level'] + optionalCreateAttrs = ['expires_at'] shortPrintAttr = 'username' @@ -2020,6 +2026,7 @@ class ProjectService(GitlabObject): canCreate = False _id_in_update_url = False _id_in_delete_url = False + getRequiresId = False requiredUrlAttrs = ['project_id', 'service_name'] _service_attrs = { @@ -2035,6 +2042,10 @@ class ProjectService(GitlabObject): 'drone-ci': (('token', 'drone_url'), ('enable_ssl_verification', )), 'emails-on-push': (('recipients', ), ('disable_diffs', 'send_from_committer_email')), + 'builds-email': (('recipients', ), ('add_pusher', + 'notify_only_broken_builds')), + 'pipelines-email': (('recipients', ), ('add_pusher', + 'notify_only_broken_builds')), 'external-wiki': (('external_wiki_url', ), tuple()), 'flowdock': (('token', ), tuple()), 'gemnasium': (('api_key', 'token', ), tuple()), @@ -2137,6 +2148,16 @@ class ProjectDeploymentManager(BaseManager): obj_cls = ProjectDeployment +class ProjectRunner(GitlabObject): + _url = '/projects/%(project_id)s/runners' + canUpdate = False + requiredCreateAttrs = ['runner_id'] + + +class ProjectRunnerManager(BaseManager): + obj_cls = ProjectRunner + + class Project(GitlabObject): _url = '/projects' _constructorTypes = {'owner': 'User', 'namespace': 'Group'} @@ -2184,6 +2205,7 @@ class Project(GitlabObject): ('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')]), @@ -2473,6 +2495,7 @@ class Runner(GitlabObject): _url = '/runners' canCreate = False optionalUpdateAttrs = ['description', 'active', 'tag_list'] + optionalListAttrs = ['scope'] class RunnerManager(BaseManager): diff --git a/tools/python_test.py b/tools/python_test.py index abfa5087b..ae5e09985 100644 --- a/tools/python_test.py +++ b/tools/python_test.py @@ -12,6 +12,13 @@ "a6WP5lTi/HJIjAl6Hu+zHgdj1XVExeH+S52EwpZf/ylTJub0Bl5gHwf/siVE48mLMI" "sqrukXTZ6Zg+8EHAIvIQwJ1dKcXe8P5IoLT7VKrbkgAnolS0I8J+uH7KtErZJb5oZh" "S4OEwsNpaXMAr+6/wWSpircV2/e7sFLlhlKBC4Iq1MpqlZ7G3p foo@bar") +DEPLOY_KEY = ("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFdRyjJQh+1niBpXqE2I8dzjG" + "MXFHlRjX9yk/UfOn075IdaockdU58sw2Ai1XIWFpZpfJkW7z+P47ZNSqm1gzeXI" + "rtKa9ZUp8A7SZe8vH4XVn7kh7bwWCUirqtn8El9XdqfkzOs/+FuViriUWoJVpA6" + "WZsDNaqINFKIA5fj/q8XQw+BcS92L09QJg9oVUuH0VVwNYbU2M2IRmSpybgC/gu" + "uWTrnCDMmLItksATifLvRZwgdI8dr+q6tbxbZknNcgEPrI2jT0hYN9ZcjNeWuyv" + "rke9IepE7SPBT41C+YtUX4dfDZDmczM1cE0YL/krdUCfuZHMa4ZS2YyNd6slufc" + "vn bar@foo") # login/password authentication gl = gitlab.Gitlab('http://localhost:8080', email=LOGIN, password=PASSWORD) @@ -161,8 +168,21 @@ readme = admin_project.files.get(file_path='README.rst', ref='master') assert(readme.decode() == 'Initial content') +data = { + 'branch_name': 'master', + 'commit_message': 'blah blah blah', + 'actions': [ + { + 'action': 'create', + 'file_path': 'blah', + 'content': 'blah' + } + ] +} +admin_project.commits.create(data) + tree = admin_project.repository_tree() -assert(len(tree) == 1) +assert(len(tree) == 2) assert(tree[0]['name'] == 'README.rst') blob = admin_project.repository_blob('master', 'README.rst') assert(blob == 'Initial content') @@ -170,6 +190,15 @@ archive2 = admin_project.repository_archive('master') assert(archive1 == archive2) +# deploy keys +deploy_key = admin_project.keys.create({'title': 'foo@bar', 'key': DEPLOY_KEY}) +project_keys = admin_project.keys.list() +assert(len(project_keys) == 1) +sudo_project.keys.enable(deploy_key.id) +assert(len(sudo_project.keys.list()) == 1) +sudo_project.keys.disable(deploy_key.id) +assert(len(sudo_project.keys.list()) == 0) + # labels label1 = admin_project.labels.create({'name': 'label1', 'color': '#778899'}) label1 = admin_project.labels.get('label1') @@ -290,6 +319,14 @@ settings = gl.notificationsettings.get() assert(settings.level == gitlab.NOTIFICATION_LEVEL_WATCH) +# services +service = admin_project.services.get(service_name='asana') +service.active = True +service.api_key = 'whatever' +service.save() +service = admin_project.services.get(service_name='asana') +assert(service.active == True) + # snippets snippets = gl.snippets.list() assert(len(snippets) == 0)