summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AUTHORS7
-rw-r--r--ChangeLog.rst41
-rw-r--r--README.rst3
-rw-r--r--RELEASE_NOTES.rst8
-rw-r--r--docs/api-objects.rst4
-rw-r--r--docs/api-usage.rst50
-rw-r--r--docs/cli.rst10
-rw-r--r--docs/gl_objects/builds.py20
-rw-r--r--docs/gl_objects/builds.rst18
-rw-r--r--docs/gl_objects/commits.rst5
-rw-r--r--docs/gl_objects/emojis.rst45
-rw-r--r--docs/gl_objects/events.rst48
-rw-r--r--docs/gl_objects/features.rst26
-rw-r--r--docs/gl_objects/groups.py50
-rw-r--r--docs/gl_objects/groups.rst138
-rw-r--r--docs/gl_objects/issues.py4
-rw-r--r--docs/gl_objects/issues.rst6
-rw-r--r--docs/gl_objects/pagesdomains.rst65
-rw-r--r--docs/gl_objects/projects.py14
-rw-r--r--docs/gl_objects/projects.rst70
-rw-r--r--docs/gl_objects/settings.rst2
-rw-r--r--docs/gl_objects/users.py18
-rw-r--r--docs/gl_objects/users.rst31
-rw-r--r--docs/install.rst2
-rw-r--r--docs/switching-to-v4.rst12
-rw-r--r--gitlab/__init__.py38
-rw-r--r--gitlab/base.py2
-rw-r--r--gitlab/config.py6
-rw-r--r--gitlab/exceptions.py4
-rw-r--r--gitlab/mixins.py5
-rw-r--r--gitlab/tests/test_base.py3
-rw-r--r--gitlab/tests/test_gitlab.py6
-rw-r--r--gitlab/tests/test_gitlabobject.py44
-rw-r--r--gitlab/tests/test_manager.py3
-rw-r--r--gitlab/tests/test_mixins.py86
-rw-r--r--gitlab/v3/objects.py15
-rw-r--r--gitlab/v4/objects.py416
-rw-r--r--setup.py9
-rwxr-xr-xtools/build_test_env.sh45
-rwxr-xr-xtools/generate_token.py67
-rw-r--r--tools/python_test_v3.py8
-rw-r--r--tools/python_test_v4.py75
42 files changed, 1080 insertions, 449 deletions
diff --git a/AUTHORS b/AUTHORS
index 7937908..ac5d28f 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -17,7 +17,10 @@ Andrew Austin <aaustin@terremark.com>
Armin Weihbold <armin.weihbold@gmail.com>
Aron Pammer <info@aronpammer.me>
Asher256 <Asher256@users.noreply.github.com>
+Bancarel Valentin <bancarel.valentin@gmail.com>
+Ben Brown <ben.brown@codethink.co.uk>
Carlo Mion <mion00@users.noreply.github.com>
+Carlos Soriano <csoriano@gnome.org>
Christian <cgumpert@users.noreply.github.com>
Christian Wenk <christian.wenk@omicronenergy.com>
Colin D Bennett <colin.bennett@harman.com>
@@ -28,6 +31,7 @@ derek-austin <derek.austin35@mailinator.com>
Diego Giovane Pasqualin <dpasqualin@c3sl.ufpr.br>
Dmytro Litvinov <litvinov.do.it@gmail.com>
Eli Sarver <eli.sarver@gmail.com>
+Eric L Frederich <eric.frederich@siemens.com>
Erik Weatherwax <erik.weatherwax@xls.xerox.com>
fgouteroux <francois.gouteroux@d2-si.eu>
Greg Allen <GregoryEAllen@users.noreply.github.com>
@@ -61,12 +65,14 @@ Mikhail Lopotkov <ms.lopotkov@tensor.ru>
Missionrulz <missionrulz@gmail.com>
Mond WAN <mondwan@users.noreply.github.com>
Nathan Giesbrecht <NathanGiesbrecht@users.noreply.github.com>
+Nathan Schmidt <nathan@cascade-softworks.com>
pa4373 <pa4373@gmail.com>
Patrick Miller <patrick@velocitywebworks.com>
Pavel Savchenko <asfaltboy@gmail.com>
Peng Xiao <xiaoquwl@gmail.com>
Pete Browne <pete.browne@localmed.com>
Peter Mosmans <support@go-forward.net>
+P. F. Chimento <philip.chimento@gmail.com>
Philipp Busch <philipp.busch@momox.biz>
Rafael Eyng <rafaeleyng@gmail.com>
Richard Hansen <rhansen@rhansen.org>
@@ -76,6 +82,7 @@ savenger <github@smahmood.de>
Stefan K. Dunkler <stefan.dun@gmail.com>
Stefan Klug <klug.stefan@gmx.de>
Stefano Mandruzzato <stefano.mandruzzato@gmail.com>
+THEBAULT Julien <julien@thebault.co>
Tim Neumann <mail@timnn.me>
Will Starms <vilhelmen@gmail.com>
Yosi Zelensky <yosyos04@gmail.com>
diff --git a/ChangeLog.rst b/ChangeLog.rst
index fe6b201..3049b9a 100644
--- a/ChangeLog.rst
+++ b/ChangeLog.rst
@@ -1,6 +1,46 @@
ChangeLog
=========
+Version 1.2.0_ - 2018-01-01
+---------------------------
+
+* Add mattermost service support
+* Add users custom attributes support
+* [doc] Fix project.triggers.create example with v4 API
+* Oauth token support
+* Remove deprecated objects/methods
+* Rework authentication args handling
+* Add support for oauth and anonymous auth in config/CLI
+* Add support for impersonation tokens API
+* Add support for user activities
+* Update user docs with gitlab URLs
+* [docs] Bad arguments in projetcs file documentation
+* Add support for user_agent_detail (issues)
+* Add a SetMixin
+* Add support for project housekeeping
+* Expected HTTP response for subscribe is 201
+* Update pagination docs for ProjectCommit
+* Add doc to get issue from iid
+* Make todo() raise GitlabTodoError on error
+* Add support for award emojis
+* Update project services docs for v4
+* Avoid sending empty update data to issue.save
+* [docstrings] Explicitly document pagination arguments
+* [docs] Add a note about password auth being removed from GitLab
+* Submanagers: allow having undefined parameters
+* ProjectFile.create(): don't modify the input data
+* Update testing tools for /session removal
+* Update groups tests
+* Allow per_page to be used with generators
+* Add groups listing attributes
+* Add support for subgroups listing
+* Add supported python versions in setup.py
+* Add support for pagesdomains
+* Add support for features flags
+* Add support for project and group custom variables
+* Add support for user/group/project filter by custom attribute
+* Respect content of REQUESTS_CA_BUNDLE and *_proxy envvars
+
Version 1.1.0_ - 2017-11-03
---------------------------
@@ -495,6 +535,7 @@ Version 0.1 - 2013-07-08
* Initial release
+.. _1.2.0: https://github.com/python-gitlab/python-gitlab/compare/1.1.0...1.2.0
.. _1.1.0: https://github.com/python-gitlab/python-gitlab/compare/1.0.2...1.1.0
.. _1.0.2: https://github.com/python-gitlab/python-gitlab/compare/1.0.1...1.0.2
.. _1.0.1: https://github.com/python-gitlab/python-gitlab/compare/1.0.0...1.0.1
diff --git a/README.rst b/README.rst
index cce2ad0..652b79f 100644
--- a/README.rst
+++ b/README.rst
@@ -7,6 +7,9 @@
.. image:: https://readthedocs.org/projects/python-gitlab/badge/?version=latest
:target: https://python-gitlab.readthedocs.org/en/latest/?badge=latest
+.. image:: https://img.shields.io/pypi/pyversions/python-gitlab.svg
+ :target: https://pypi.python.org/pypi/python-gitlab
+
Python GitLab
=============
diff --git a/RELEASE_NOTES.rst b/RELEASE_NOTES.rst
index a9008f7..da2545f 100644
--- a/RELEASE_NOTES.rst
+++ b/RELEASE_NOTES.rst
@@ -4,9 +4,17 @@ Release notes
This page describes important changes between python-gitlab releases.
+Changes from 1.2 to 1.3
+=======================
+
+* ``gitlab.Gitlab`` objects can be used as context managers in a ``with``
+ block.
+
Changes from 1.1 to 1.2
=======================
+* python-gitlab now respects the ``*_proxy``, ``REQUESTS_CA_BUNDLE`` and
+ ``CURL_CA_BUNDLE`` environment variables (#352)
* The following deprecated methods and objects have been removed:
* gitlab.v3.object ``Key`` and ``KeyManager`` objects: use ``DeployKey`` and
diff --git a/docs/api-objects.rst b/docs/api-objects.rst
index e549924..f2e72e2 100644
--- a/docs/api-objects.rst
+++ b/docs/api-objects.rst
@@ -6,6 +6,7 @@ API examples
:maxdepth: 1
gl_objects/access_requests
+ gl_objects/emojis
gl_objects/branches
gl_objects/protected_branches
gl_objects/messages
@@ -14,6 +15,8 @@ API examples
gl_objects/deploy_keys
gl_objects/deployments
gl_objects/environments
+ gl_objects/events
+ gl_objects/features
gl_objects/groups
gl_objects/issues
gl_objects/labels
@@ -21,6 +24,7 @@ API examples
gl_objects/mrs
gl_objects/namespaces
gl_objects/milestones
+ gl_objects/pagesdomains
gl_objects/projects
gl_objects/runners
gl_objects/settings
diff --git a/docs/api-usage.rst b/docs/api-usage.rst
index f60c0dc..190482f 100644
--- a/docs/api-usage.rst
+++ b/docs/api-usage.rst
@@ -7,7 +7,7 @@ python-gitlab supports both GitLab v3 and v4 APIs.
v3 being deprecated by GitLab, its support in python-gitlab will be minimal.
The development team will focus on v4.
-v3 is still the default API used by python-gitlab, for compatibility reasons.
+v4 is the default API used by python-gitlab since version 1.3.0.
``gitlab.Gitlab`` class
@@ -19,13 +19,13 @@ To connect to a GitLab server, create a ``gitlab.Gitlab`` object:
import gitlab
- # private token authentication
+ # private token or personal token authentication
gl = gitlab.Gitlab('http://10.0.0.1', private_token='JVNSESs8EwWRx5yDxM5q')
# oauth token authentication
gl = gitlab.Gitlab('http://10.0.0.1', oauth_token='my_long_token_here')
- # username/password authentication
+ # username/password authentication (for GitLab << 10.2)
gl = gitlab.Gitlab('http://10.0.0.1', email='jdoe', password='s3cr3t')
# anonymous gitlab instance, read-only for public resources
@@ -44,25 +44,38 @@ You can also use configuration files to create ``gitlab.Gitlab`` objects:
See the :ref:`cli_configuration` section for more information about
configuration files.
+Note on password authentication
+-------------------------------
+
+The ``/session`` API endpoint used for username/password authentication has
+been removed from GitLab in version 10.2, and is not available on gitlab.com
+anymore. Personal token authentication is the prefered authentication method.
+
+If you need username/password authentication, you can use cookie-based
+authentication. You can use the web UI form to authenticate, retrieve cookies,
+and then use a custom ``requests.Session`` object to connect to the GitLab API.
+The following code snippet demonstrates how to automate this:
+https://gist.github.com/gpocentek/bd4c3fbf8a6ce226ebddc4aad6b46c0a.
+
+See `issue 380 <https://github.com/python-gitlab/python-gitlab/issues/380>`_
+for a detailed discussion.
API version
===========
-``python-gitlab`` uses the v3 GitLab API by default. Use the ``api_version``
-parameter to switch to v4:
+``python-gitlab`` uses the v4 GitLab API by default. Use the ``api_version``
+parameter to switch to v3:
.. code-block:: python
import gitlab
- gl = gitlab.Gitlab('http://10.0.0.1', 'JVNSESs8EwWRx5yDxM5q', api_version=4)
+ gl = gitlab.Gitlab('http://10.0.0.1', 'JVNSESs8EwWRx5yDxM5q', api_version=3)
.. warning::
The python-gitlab API is not the same for v3 and v4. Make sure to read
- :ref:`switching_to_v4` before upgrading.
-
- v4 will become the default in python-gitlab.
+ :ref:`switching_to_v4` if you are upgrading from v3.
Managers
========
@@ -203,7 +216,7 @@ listing methods support the ``page`` and ``per_page`` parameters:
.. note::
- The first page is page 1, not page 0.
+ The first page is page 1, not page 0, except for project commits in v3 API.
By default GitLab does not return the complete list of items. Use the ``all``
parameter to get all the items when using listing methods:
@@ -259,6 +272,23 @@ HTTP requests to the Gitlab servers.
You can provide your own ``Session`` object with custom configuration when
you create a ``Gitlab`` object.
+Context manager
+---------------
+
+You can use ``Gitlab`` objects as context managers. This makes sure that the
+``requests.Session`` object associated with a ``Gitlab`` instance is always
+properly closed when you exit a ``with`` block:
+
+.. code-block:: python
+
+ with gitlab.Gitlab(host, token) as gl:
+ gl.projects.list()
+
+.. warning::
+
+ The context manager will also close the custom ``Session`` object you might
+ have used to build a ``Gitlab`` instance.
+
Proxy configuration
-------------------
diff --git a/docs/cli.rst b/docs/cli.rst
index f75a46a..591761c 100644
--- a/docs/cli.rst
+++ b/docs/cli.rst
@@ -37,12 +37,11 @@ example:
default = somewhere
ssl_verify = true
timeout = 5
- api_version = 3
[somewhere]
url = https://some.whe.re
private_token = vTbFeqJYCY3sibBP7BZM
- api_version = 4
+ api_version = 3
[elsewhere]
url = http://else.whe.re:8080
@@ -69,6 +68,9 @@ parameters. You can override the values in each GitLab server section.
* - ``timeout``
- Integer
- Number of seconds to wait for an answer before failing.
+ * - ``api_version``
+ - ``3`` ou ``4``
+ - The API version to use to make queries. Requires python-gitlab >= 1.3.0.
You must define the ``url`` in each GitLab server section.
@@ -90,8 +92,8 @@ limited permissions.
- An Oauth token for authentication. The Gitlab server must be configured
to support this authentication method.
* - ``api_version``
- - GitLab API version to use (``3`` or ``4``). Defaults to ``3`` for now,
- but will switch to ``4`` eventually.
+ - GitLab API version to use (``3`` or ``4``). Defaults to ``4`` since
+ version 1.3.0.
* - ``http_username``
- Username for optional HTTP authentication
* - ``http_password``
diff --git a/docs/gl_objects/builds.py b/docs/gl_objects/builds.py
index 803edc6..a5d2005 100644
--- a/docs/gl_objects/builds.py
+++ b/docs/gl_objects/builds.py
@@ -4,8 +4,8 @@ g_variables = group.variables.list()
# end var list
# var get
-p_var = project.variables.get(var_key)
-g_var = group.variables.get(var_key)
+p_var = project.variables.get('key_name')
+g_var = group.variables.get('key_name')
# end var get
# var create
@@ -19,8 +19,8 @@ var.save()
# end var update
# var delete
-project.variables.delete(var_key)
-group.variables.delete(var_key)
+project.variables.delete('key_name')
+group.variables.delete('key_name')
# or
var.delete()
# end var delete
@@ -55,10 +55,18 @@ commit = gl.project_commits.get(commit_sha, project_id=1)
builds = commit.builds()
# end commit list
-# get
+# pipeline list get
+# v4 only
+project = gl.projects.get(project_id)
+pipeline = project.pipelines.get(pipeline_id)
+jobs = pipeline.jobs.list() # gets all jobs in pipeline
+job = pipeline.jobs.get(job_id) # gets one job from pipeline
+# end pipeline list get
+
+# get job
project.builds.get(build_id) # v3
project.jobs.get(job_id) # v4
-# end get
+# end get job
# artifacts
build_or_job.artifacts()
diff --git a/docs/gl_objects/builds.rst b/docs/gl_objects/builds.rst
index 1c95eb1..b0f3e22 100644
--- a/docs/gl_objects/builds.rst
+++ b/docs/gl_objects/builds.rst
@@ -122,8 +122,9 @@ Remove a variable:
Builds/Jobs
===========
-Builds/Jobs are associated to projects and commits. They provide information on
-the builds/jobs that have been run, and methods to manipulate them.
+Builds/Jobs are associated to projects, pipelines and commits. They provide
+information on the builds/jobs that have been run, and methods to manipulate
+them.
Reference
---------
@@ -169,11 +170,20 @@ To list builds for a specific commit, create a
:start-after: # commit list
:end-before: # end commit list
+To list builds for a specific pipeline or get a single job within a specific
+pipeline, create a
+:class:`~gitlab.v4.objects.ProjectPipeline` object and use its
+:attr:`~gitlab.v4.objects.ProjectPipeline.jobs` method (v4 only):
+
+.. literalinclude:: builds.py
+ :start-after: # pipeline list get
+ :end-before: # end pipeline list get
+
Get a job:
.. literalinclude:: builds.py
- :start-after: # get
- :end-before: # end get
+ :start-after: # get job
+ :end-before: # end get job
Get a job artifact:
diff --git a/docs/gl_objects/commits.rst b/docs/gl_objects/commits.rst
index 9267cae..8a32709 100644
--- a/docs/gl_objects/commits.rst
+++ b/docs/gl_objects/commits.rst
@@ -23,6 +23,11 @@ Reference
* GitLab API: https://docs.gitlab.com/ce/api/commits.html
+.. warning::
+
+ Pagination starts at page 0 in v3, but starts at page 1 in v4 (like all the
+ v4 endpoints).
+
Examples
--------
diff --git a/docs/gl_objects/emojis.rst b/docs/gl_objects/emojis.rst
new file mode 100644
index 0000000..179141f
--- /dev/null
+++ b/docs/gl_objects/emojis.rst
@@ -0,0 +1,45 @@
+############
+Award Emojis
+############
+
+Reference
+---------
+
+* v4 API:
+
+ + :class:`gitlab.v4.objects.ProjectIssueAwardEmoji`
+ + :class:`gitlab.v4.objects.ProjectIssueNoteAwardEmoji`
+ + :class:`gitlab.v4.objects.ProjectMergeRequestAwardEmoji`
+ + :class:`gitlab.v4.objects.ProjectMergeRequestNoteAwardEmoji`
+ + :class:`gitlab.v4.objects.ProjectSnippetAwardEmoji`
+ + :class:`gitlab.v4.objects.ProjectSnippetNoteAwardEmoji`
+ + :class:`gitlab.v4.objects.ProjectIssueAwardEmojiManager`
+ + :class:`gitlab.v4.objects.ProjectIssueNoteAwardEmojiManager`
+ + :class:`gitlab.v4.objects.ProjectMergeRequestAwardEmojiManager`
+ + :class:`gitlab.v4.objects.ProjectMergeRequestNoteAwardEmojiManager`
+ + :class:`gitlab.v4.objects.ProjectSnippetAwardEmojiManager`
+ + :class:`gitlab.v4.objects.ProjectSnippetNoteAwardEmojiManager`
+
+
+* GitLab API: https://docs.gitlab.com/ce/api/award_emoji.html
+
+Examples
+--------
+
+List emojis for a resource::
+
+ emojis = obj.awardemojis.list()
+
+Get a single emoji::
+
+ emoji = obj.awardemojis.get(emoji_id)
+
+Add (create) an emoji::
+
+ emoji = obj.awardemojis.create({'name': 'tractor'})
+
+Delete an emoji::
+
+ emoji.delete
+ # or
+ obj.awardemojis.delete(emoji_id)
diff --git a/docs/gl_objects/events.rst b/docs/gl_objects/events.rst
new file mode 100644
index 0000000..807dcad
--- /dev/null
+++ b/docs/gl_objects/events.rst
@@ -0,0 +1,48 @@
+######
+Events
+######
+
+Reference
+---------
+
+* v4 API:
+
+ + :class:`gitlab.v4.objects.Event`
+ + :class:`gitlab.v4.objects.EventManager`
+ + :attr:`gitlab.Gitlab.events`
+ + :class:`gitlab.v4.objects.ProjectEvent`
+ + :class:`gitlab.v4.objects.ProjectEventManager`
+ + :attr:`gitlab.v4.objects.Project.events`
+ + :class:`gitlab.v4.objects.UserEvent`
+ + :class:`gitlab.v4.objects.UserEventManager`
+ + :attr:`gitlab.v4.objects.User.events`
+
+* v3 API (projects events only):
+
+ + :class:`gitlab.v3.objects.ProjectEvent`
+ + :class:`gitlab.v3.objects.ProjectEventManager`
+ + :attr:`gitlab.v3.objects.Project.events`
+ + :attr:`gitlab.Gitlab.project_events`
+
+* GitLab API: https://docs.gitlab.com/ce/api/events.html
+
+Examples
+--------
+
+You can list events for an entire Gitlab instance (admin), users and projects.
+You can filter you events you want to retrieve using the ``action`` and
+``target_type`` attributes. The possibole values for these attributes are
+available on `the gitlab documentation
+<https://docs.gitlab.com/ce/api/events.html>`_.
+
+List all the events (paginated)::
+
+ events = gl.events.list()
+
+List the issue events on a project::
+
+ events = project.events.list(target_type='issue')
+
+List the user events::
+
+ events = project.events.list()
diff --git a/docs/gl_objects/features.rst b/docs/gl_objects/features.rst
new file mode 100644
index 0000000..201d072
--- /dev/null
+++ b/docs/gl_objects/features.rst
@@ -0,0 +1,26 @@
+##############
+Features flags
+##############
+
+Reference
+---------
+
+* v4 API:
+
+ + :class:`gitlab.v4.objects.Feature`
+ + :class:`gitlab.v4.objects.FeatureManager`
+ + :attr:`gitlab.Gitlab.features`
+
+* GitLab API: https://docs.gitlab.com/ce/api/features.html
+
+Examples
+--------
+
+List features::
+
+ features = gl.features.list()
+
+Create or set a feature::
+
+ feature = gl.features.set(feature_name, True)
+ feature = gl.features.set(feature_name, 30)
diff --git a/docs/gl_objects/groups.py b/docs/gl_objects/groups.py
deleted file mode 100644
index f1a2a8f..0000000
--- a/docs/gl_objects/groups.py
+++ /dev/null
@@ -1,50 +0,0 @@
-# list
-groups = gl.groups.list()
-# end list
-
-# get
-group = gl.groups.get(group_id)
-# end get
-
-# projects list
-projects = group.projects.list()
-# end projects list
-
-# create
-group = gl.groups.create({'name': 'group1', 'path': 'group1'})
-# end create
-
-# update
-group.description = 'My awesome group'
-group.save()
-# end update
-
-# delete
-gl.group.delete(group_id)
-# or
-group.delete()
-# end delete
-
-# member list
-members = group.members.list()
-# end member list
-
-# member get
-members = group.members.get(member_id)
-# end member get
-
-# member create
-member = group.members.create({'user_id': user_id,
- 'access_level': gitlab.GUEST_ACCESS})
-# end member create
-
-# member update
-member.access_level = gitlab.DEVELOPER_ACCESS
-member.save()
-# end member update
-
-# member delete
-group.members.delete(member_id)
-# or
-member.delete()
-# end member delete
diff --git a/docs/gl_objects/groups.rst b/docs/gl_objects/groups.rst
index 5e413af..493f5d0 100644
--- a/docs/gl_objects/groups.rst
+++ b/docs/gl_objects/groups.rst
@@ -25,23 +25,17 @@ Reference
Examples
--------
-List the groups:
+List the groups::
-.. literalinclude:: groups.py
- :start-after: # list
- :end-before: # end list
+ groups = gl.groups.list()
-Get a group's detail:
+Get a group's detail::
-.. literalinclude:: groups.py
- :start-after: # get
- :end-before: # end get
+ group = gl.groups.get(group_id)
-List a group's projects:
+List a group's projects::
-.. literalinclude:: groups.py
- :start-after: # projects list
- :end-before: # end projects list
+ projects = group.projects.list()
You can filter and sort the result using the following parameters:
@@ -54,23 +48,85 @@ You can filter and sort the result using the following parameters:
* ``sort``: sort order: ``asc`` or ``desc``
* ``ci_enabled_first``: return CI enabled groups first
-Create a group:
+Create a group::
-.. literalinclude:: groups.py
- :start-after: # create
- :end-before: # end create
+ group = gl.groups.create({'name': 'group1', 'path': 'group1'})
-Update a group:
+Update a group::
-.. literalinclude:: groups.py
- :start-after: # update
- :end-before: # end update
+ group.description = 'My awesome group'
+ group.save()
-Remove a group:
+Remove a group::
-.. literalinclude:: groups.py
- :start-after: # delete
- :end-before: # end delete
+ gl.group.delete(group_id)
+ # or
+ group.delete()
+
+Subgroups
+=========
+
+Reference
+---------
+
+* v4 API:
+
+ + :class:`gitlab.v4.objects.GroupSubgroup`
+ + :class:`gitlab.v4.objects.GroupSubgroupManager`
+ + :attr:`gitlab.v4.objects.Group.subgroups`
+
+Examples
+--------
+
+List the subgroups for a group::
+
+ subgroups = group.subgroups.list()
+
+ # The GroupSubgroup objects don't expose the same API as the Group
+ # objects. If you need to manipulate a subgroup as a group, create a new
+ # Group object:
+ real_group = gl.groups.get(subgroup_id, lazy=True)
+ real_group.issues.list()
+
+Group custom attributes
+=======================
+
+Reference
+---------
+
+* v4 API:
+
+ + :class:`gitlab.v4.objects.GroupCustomAttribute`
+ + :class:`gitlab.v4.objects.GroupCustomAttributeManager`
+ + :attr:`gitlab.v4.objects.Group.customattributes`
+
+* GitLab API: https://docs.gitlab.com/ce/api/custom_attributes.html
+
+Examples
+--------
+
+List custom attributes for a group::
+
+ attrs = group.customattributes.list()
+
+Get a custom attribute for a group::
+
+ attr = group.customattributes.get(attr_key)
+
+Set (create or update) a custom attribute for a group::
+
+ attr = group.customattributes.set(attr_key, attr_value)
+
+Delete a custom attribute for a group::
+
+ attr.delete()
+ # or
+ group.customattributes.delete(attr_key)
+
+Search groups by custom attribute::
+
+ group.customattributes.set('role': 'admin')
+ gl.groups.list(custom_attributes={'role': 'admin'})
Group members
=============
@@ -105,32 +161,26 @@ Reference
Examples
--------
-List group members:
+List group members::
-.. literalinclude:: groups.py
- :start-after: # member list
- :end-before: # end member list
+ members = group.members.list()
-Get a group member:
+Get a group member::
-.. literalinclude:: groups.py
- :start-after: # member get
- :end-before: # end member get
+ members = group.members.get(member_id)
-Add a member to the group:
+Add a member to the group::
-.. literalinclude:: groups.py
- :start-after: # member create
- :end-before: # end member create
+ member = group.members.create({'user_id': user_id,
+ 'access_level': gitlab.GUEST_ACCESS})
-Update a member (change the access level):
+Update a member (change the access level)::
-.. literalinclude:: groups.py
- :start-after: # member update
- :end-before: # end member update
+ member.access_level = gitlab.DEVELOPER_ACCESS
+ member.save()
-Remove a member from the group:
+Remove a member from the group::
-.. literalinclude:: groups.py
- :start-after: # member delete
- :end-before: # end member delete
+ group.members.delete(member_id)
+ # or
+ member.delete()
diff --git a/docs/gl_objects/issues.py b/docs/gl_objects/issues.py
index 2e4645e..ef27e07 100644
--- a/docs/gl_objects/issues.py
+++ b/docs/gl_objects/issues.py
@@ -28,6 +28,10 @@ issues = project.issues.list(order_by='created_at', sort='desc')
issue = project.issues.get(issue_id)
# end project issues get
+# project issues get from iid
+issue = project.issues.list(iid=issue_iid)[0]
+# end project issues get from iid
+
# project issues create
issue = project.issues.create({'title': 'I have a bug',
'description': 'Something useful here.'})
diff --git a/docs/gl_objects/issues.rst b/docs/gl_objects/issues.rst
index 4384ba9..136d8b8 100644
--- a/docs/gl_objects/issues.rst
+++ b/docs/gl_objects/issues.rst
@@ -104,6 +104,12 @@ Get a project issue:
:start-after: # project issues get
:end-before: # end project issues get
+Get a project issue from its `iid` (v3 only. Issues are retrieved by iid in V4 by default):
+
+.. literalinclude:: issues.py
+ :start-after: # project issues get from iid
+ :end-before: # end project issues get from iid
+
Create a new issue:
.. literalinclude:: issues.py
diff --git a/docs/gl_objects/pagesdomains.rst b/docs/gl_objects/pagesdomains.rst
new file mode 100644
index 0000000..d6b39c7
--- /dev/null
+++ b/docs/gl_objects/pagesdomains.rst
@@ -0,0 +1,65 @@
+#############
+Pages domains
+#############
+
+Admin
+=====
+
+References
+----------
+
+* v4 API:
+
+ + :class:`gitlab.v4.objects.PagesDomain`
+ + :class:`gitlab.v4.objects.PagesDomainManager`
+ + :attr:`gitlab.Gitlab.pagesdomains`
+
+* GitLab API: https://docs.gitlab.com/ce/api/pages_domains.html#list-all-pages-domains
+
+Examples
+--------
+
+List all the existing domains (admin only)::
+
+ domains = gl.pagesdomains.list()
+
+Project pages domain
+====================
+
+References
+----------
+
+* v4 API:
+
+ + :class:`gitlab.v4.objects.ProjectPagesDomain`
+ + :class:`gitlab.v4.objects.ProjectPagesDomainManager`
+ + :attr:`gitlab.v4.objects.Project.pagesdomains`
+
+* GitLab API: https://docs.gitlab.com/ce/api/pages_domains.html#list-pages-domains
+
+Examples
+--------
+
+List domains for a project::
+
+ domains = project.pagesdomains.list()
+
+Get a single domain::
+
+ domain = project.pagesdomains.get('d1.example.com')
+
+Create a new domain::
+
+ domain = project.pagesdomains.create({'domain': 'd2.example.com})
+
+Update an existing domain::
+
+ domain.certificate = open('d2.crt').read()
+ domain.key = open('d2.key').read()
+ domain.save()
+
+Delete an existing domain::
+
+ domain.delete
+ # or
+ project.pagesdomains.delete('d2.example.com')
diff --git a/docs/gl_objects/projects.py b/docs/gl_objects/projects.py
index 515397f..a633ee8 100644
--- a/docs/gl_objects/projects.py
+++ b/docs/gl_objects/projects.py
@@ -33,6 +33,7 @@ project = gl.projects.create({'name': 'project1'})
# user create
alice = gl.users.list(username='alice')[0]
user_project = alice.projects.create({'name': 'project'})
+user_projects = alice.projects.list()
# end user create
# update
@@ -68,12 +69,6 @@ project.archive()
project.unarchive()
# end archive
-# events list
-gl.project_events.list(project_id=1)
-# or
-project.events.list()
-# end events list
-
# members list
members = project.members.list()
# end members list
@@ -229,7 +224,7 @@ tags = project.tags.list()
# end tags list
# tags get
-tags = project.tags.list('1.0')
+tag = project.tags.get('1.0')
# end tags get
# tags create
@@ -305,8 +300,11 @@ note.delete()
# end notes delete
# service get
+# For v3
service = project.services.get(service_name='asana', project_id=1)
-# display it's status (enabled/disabled)
+# For v4
+service = project.services.get('asana')
+# display its status (enabled/disabled)
print(service.active)
# end service get
diff --git a/docs/gl_objects/projects.rst b/docs/gl_objects/projects.rst
index aaf0699..0c556f4 100644
--- a/docs/gl_objects/projects.rst
+++ b/docs/gl_objects/projects.rst
@@ -172,6 +172,46 @@ Get a list of users for the repository:
:start-after: # users list
:end-before: # end users list
+Project custom attributes
+=========================
+
+Reference
+---------
+
+* v4 API:
+
+ + :class:`gitlab.v4.objects.ProjectCustomAttribute`
+ + :class:`gitlab.v4.objects.ProjectCustomAttributeManager`
+ + :attr:`gitlab.v4.objects.Project.customattributes`
+
+* GitLab API: https://docs.gitlab.com/ce/api/custom_attributes.html
+
+Examples
+--------
+
+List custom attributes for a project::
+
+ attrs = project.customattributes.list()
+
+Get a custom attribute for a project::
+
+ attr = project.customattributes.get(attr_key)
+
+Set (create or update) a custom attribute for a project::
+
+ attr = project.customattributes.set(attr_key, attr_value)
+
+Delete a custom attribute for a project::
+
+ attr.delete()
+ # or
+ project.customattributes.delete(attr_key)
+
+Search projects by custom attribute::
+
+ project.customattributes.set('type': 'internal')
+ gl.projects.list(custom_attributes={'type': 'internal'})
+
Project files
=============
@@ -444,36 +484,6 @@ Delete a note for a resource:
:start-after: # notes delete
:end-before: # end notes delete
-Project events
-==============
-
-Reference
----------
-
-* v4 API:
-
- + :class:`gitlab.v4.objects.ProjectEvent`
- + :class:`gitlab.v4.objects.ProjectEventManager`
- + :attr:`gitlab.v4.objects.Project.events`
-
-* v3 API:
-
- + :class:`gitlab.v3.objects.ProjectEvent`
- + :class:`gitlab.v3.objects.ProjectEventManager`
- + :attr:`gitlab.v3.objects.Project.events`
- + :attr:`gitlab.Gitlab.project_events`
-
-* GitLab API: https://docs.gitlab.com/ce/api/repository_files.html
-
-Examples
---------
-
-List the project events:
-
-.. literalinclude:: projects.py
- :start-after: # events list
- :end-before: # end events list
-
Project members
===============
diff --git a/docs/gl_objects/settings.rst b/docs/gl_objects/settings.rst
index 5f0e92f..cf3fd4d 100644
--- a/docs/gl_objects/settings.rst
+++ b/docs/gl_objects/settings.rst
@@ -17,7 +17,7 @@ Reference
+ :class:`gitlab.v3.objects.ApplicationSettingsManager`
+ :attr:`gitlab.Gitlab.settings`
-* GitLab API: https://docs.gitlab.com/ce/api/commits.html
+* GitLab API: https://docs.gitlab.com/ce/api/settings.html
Examples
--------
diff --git a/docs/gl_objects/users.py b/docs/gl_objects/users.py
index e452217..842e35d 100644
--- a/docs/gl_objects/users.py
+++ b/docs/gl_objects/users.py
@@ -98,24 +98,6 @@ gl.auth()
current_user = gl.user
# end currentuser get
-# ca list
-attrs = user.customeattributes.list()
-# end ca list
-
-# ca get
-attr = user.customeattributes.get(attr_key)
-# end ca get
-
-# ca set
-attr = user.customeattributes.set(attr_key, attr_value)
-# end ca set
-
-# ca delete
-attr.delete()
-# or
-user.customeattributes.delete(attr_key)
-# end ca delete
-
# it list
i_t = user.impersonationtokens.list(state='active')
i_t = user.impersonationtokens.list(state='inactive')
diff --git a/docs/gl_objects/users.rst b/docs/gl_objects/users.rst
index e520c9b..63609db 100644
--- a/docs/gl_objects/users.rst
+++ b/docs/gl_objects/users.rst
@@ -89,29 +89,28 @@ References
Examples
--------
-List custom attributes for a user:
+List custom attributes for a user::
-.. literalinclude:: users.py
- :start-after: # ca list
- :end-before: # end ca list
+ attrs = user.customattributes.list()
-Get a custom attribute for a user:
+Get a custom attribute for a user::
-.. literalinclude:: users.py
- :start-after: # ca get
- :end-before: # end ca get
+ attr = user.customattributes.get(attr_key)
-Set (create or update) a custom attribute for a user:
+Set (create or update) a custom attribute for a user::
-.. literalinclude:: users.py
- :start-after: # ca set
- :end-before: # end ca set
+ attr = user.customattributes.set(attr_key, attr_value)
-Delete a custom attribute for a user:
+Delete a custom attribute for a user::
-.. literalinclude:: users.py
- :start-after: # ca list
- :end-before: # end ca list
+ attr.delete()
+ # or
+ user.customattributes.delete(attr_key)
+
+Search users by custom attribute::
+
+ user.customattributes.set('role': 'QA')
+ gl.users.list(custom_attributes={'role': 'QA'})
User impersonation tokens
=========================
diff --git a/docs/install.rst b/docs/install.rst
index 1bc6d17..4998320 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -2,7 +2,7 @@
Installation
############
-``python-gitlab`` is compatible with python 2 and 3.
+``python-gitlab`` is compatible with Python 2.7 and 3.4+.
Use :command:`pip` to install the latest stable version of ``python-gitlab``:
diff --git a/docs/switching-to-v4.rst b/docs/switching-to-v4.rst
index fff9573..ef21060 100644
--- a/docs/switching-to-v4.rst
+++ b/docs/switching-to-v4.rst
@@ -16,12 +16,12 @@ http://gitlab.com.
Using the v4 API
================
-To use the new v4 API, explicitly define ``api_version` `in the ``Gitlab``
-constructor:
+python-gitlab uses the v4 API by default since the 1.3.0 release. To use the
+old v3 API, explicitly define ``api_version`` in the ``Gitlab`` constructor:
.. code-block:: python
- gl = gitlab.Gitlab(..., api_version=4)
+ gl = gitlab.Gitlab(..., api_version=3)
If you use the configuration file, also explicitly define the version:
@@ -30,13 +30,13 @@ If you use the configuration file, also explicitly define the version:
[my_gitlab]
...
- api_version = 4
+ api_version = 3
Changes between v3 and v4 API
=============================
-For a list of GtiLab (upstream) API changes, see
+For a list of GitLab (upstream) API changes, see
https://docs.gitlab.com/ce/api/v3_to_v4.html.
The ``python-gitlab`` API reflects these changes. But also consider the
@@ -95,7 +95,7 @@ following important changes in the python API:
This will make only one API call, instead of two if ``lazy`` is not used.
-* The :class:`~gitlab.Gitlab` folowwing methods should not be used anymore for
+* The following :class:`~gitlab.Gitlab` methods should not be used anymore for
v4:
+ ``list()``
diff --git a/gitlab/__init__.py b/gitlab/__init__.py
index aac4837..c909f9f 100644
--- a/gitlab/__init__.py
+++ b/gitlab/__init__.py
@@ -34,7 +34,7 @@ from gitlab.exceptions import * # noqa
from gitlab.v3.objects import * # noqa
__title__ = 'python-gitlab'
-__version__ = '1.1.0'
+__version__ = '1.2.0'
__author__ = 'Gauvain Pocentek'
__email__ = 'gauvain@pocentek.net'
__license__ = 'LGPL3'
@@ -73,7 +73,7 @@ class Gitlab(object):
def __init__(self, url, private_token=None, oauth_token=None, email=None,
password=None, ssl_verify=True, http_username=None,
- http_password=None, timeout=None, api_version='3',
+ http_password=None, timeout=None, api_version='4',
session=None):
self._api_version = str(api_version)
@@ -125,6 +125,9 @@ class Gitlab(object):
self.teams = objects.TeamManager(self)
else:
self.dockerfiles = objects.DockerfileManager(self)
+ self.events = objects.EventManager(self)
+ self.features = objects.FeatureManager(self)
+ self.pagesdomains = objects.PagesDomainManager(self)
self.user_activities = objects.UserActivitiesManager(self)
if self._api_version == '3':
@@ -144,6 +147,12 @@ class Gitlab(object):
manager = getattr(objects, cls_name)(self)
setattr(self, var_name, manager)
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *args):
+ self.session.close()
+
def __getstate__(self):
state = self.__dict__.copy()
state.pop('_objects')
@@ -640,8 +649,22 @@ class Gitlab(object):
return parsed._replace(path=new_path).geturl()
url = self._build_url(path)
- params = query_data.copy()
- params.update(kwargs)
+
+ def copy_dict(dest, src):
+ for k, v in src.items():
+ if isinstance(v, dict):
+ # Transform dict values in new attributes. For example:
+ # custom_attributes: {'foo', 'bar'} =>
+ # custom_attributes['foo']: 'bar'
+ for dict_k, dict_v in v.items():
+ dest['%s[%s]' % (k, dict_k)] = dict_v
+ else:
+ dest[k] = v
+
+ params = {}
+ copy_dict(params, query_data)
+ copy_dict(params, kwargs)
+
opts = self._get_session_opts(content_type='application/json')
# don't set the content-type header when uploading files
@@ -661,8 +684,9 @@ class Gitlab(object):
files=files, **opts)
prepped = self.session.prepare_request(req)
prepped.url = sanitized_url(prepped.url)
- result = self.session.send(prepped, stream=streamed, verify=verify,
- timeout=timeout)
+ settings = self.session.merge_environment_settings(
+ prepped.url, {}, streamed, verify, None)
+ result = self.session.send(prepped, timeout=timeout, **settings)
if 200 <= result.status_code < 300:
return result
@@ -743,7 +767,7 @@ class Gitlab(object):
if get_all is True:
return list(GitlabList(self, url, query_data, **kwargs))
- if 'page' in kwargs or 'per_page' in kwargs or as_list is True:
+ if 'page' in kwargs or as_list is True:
# pagination requested, we return a list
return list(GitlabList(self, url, query_data, get_next=False,
**kwargs))
diff --git a/gitlab/base.py b/gitlab/base.py
index ec5f698..fd79c53 100644
--- a/gitlab/base.py
+++ b/gitlab/base.py
@@ -764,7 +764,7 @@ class RESTManager(object):
if self._parent is None or not hasattr(self, '_from_parent_attrs'):
return path
- data = {self_attr: getattr(self._parent, parent_attr)
+ data = {self_attr: getattr(self._parent, parent_attr, None)
for self_attr, parent_attr in self._from_parent_attrs.items()}
self._parent_attrs = data
return path % data
diff --git a/gitlab/config.py b/gitlab/config.py
index 9cf208c..0f4c424 100644
--- a/gitlab/config.py
+++ b/gitlab/config.py
@@ -128,7 +128,11 @@ class GitlabConfigParser(object):
except Exception:
pass
- self.api_version = '3'
+ self.api_version = '4'
+ try:
+ self.api_version = self._config.get('global', 'api_version')
+ except Exception:
+ pass
try:
self.api_version = self._config.get(self.gitlab_id, 'api_version')
except Exception:
diff --git a/gitlab/exceptions.py b/gitlab/exceptions.py
index 9a423dd..5825d23 100644
--- a/gitlab/exceptions.py
+++ b/gitlab/exceptions.py
@@ -193,6 +193,10 @@ class GitlabHousekeepingError(GitlabOperationError):
pass
+class GitlabOwnershipError(GitlabOperationError):
+ pass
+
+
def raise_error_from_response(response, error, expected_code=200):
"""Tries to parse gitlab error message from response and raises error.
diff --git a/gitlab/mixins.py b/gitlab/mixins.py
index c9243ed..cb35efc 100644
--- a/gitlab/mixins.py
+++ b/gitlab/mixins.py
@@ -242,7 +242,7 @@ class SetMixin(object):
GitlabSetError: If an error occured
Returns:
- UserCustomAttribute: The created/updated user attribute
+ obj: The created/updated attribute
"""
path = '%s/%s' % (self.path, key.replace('/', '%2F'))
data = {'value': value}
@@ -303,6 +303,9 @@ class SaveMixin(object):
GitlabUpdateError: If the server cannot perform the request
"""
updated_data = self._get_updated_data()
+ # Nothing to update. Server fails if sent an empty dict.
+ if not updated_data:
+ return
# call the manager
obj_id = self.get_id()
diff --git a/gitlab/tests/test_base.py b/gitlab/tests/test_base.py
index 31dd967..36cb63b 100644
--- a/gitlab/tests/test_base.py
+++ b/gitlab/tests/test_base.py
@@ -61,9 +61,6 @@ class TestRESTManager(unittest.TestCase):
mgr = MGR(FakeGitlab(), parent=Parent())
self.assertEqual(mgr._computed_path, '/tests/42/cases')
- self.assertRaises(AttributeError, MGR, FakeGitlab(),
- parent=BrokenParent())
-
def test_path_property(self):
class MGR(base.RESTManager):
_path = '/tests'
diff --git a/gitlab/tests/test_gitlab.py b/gitlab/tests/test_gitlab.py
index d33df99..1a1f3d8 100644
--- a/gitlab/tests/test_gitlab.py
+++ b/gitlab/tests/test_gitlab.py
@@ -53,7 +53,7 @@ class TestGitlabRawMethods(unittest.TestCase):
def setUp(self):
self.gl = Gitlab("http://localhost", private_token="private_token",
email="testuser@test.com", password="testpassword",
- ssl_verify=True)
+ ssl_verify=True, api_version=3)
@urlmatch(scheme="http", netloc="localhost", path="/api/v3/known_path",
method="get")
@@ -454,7 +454,7 @@ class TestGitlabMethods(unittest.TestCase):
def setUp(self):
self.gl = Gitlab("http://localhost", private_token="private_token",
email="testuser@test.com", password="testpassword",
- ssl_verify=True)
+ ssl_verify=True, api_version=3)
def test_list(self):
@urlmatch(scheme="http", netloc="localhost",
@@ -938,7 +938,7 @@ class TestGitlab(unittest.TestCase):
def setUp(self):
self.gl = Gitlab("http://localhost", private_token="private_token",
email="testuser@test.com", password="testpassword",
- ssl_verify=True)
+ ssl_verify=True, api_version=3)
def test_pickability(self):
original_gl_objects = self.gl._objects
diff --git a/gitlab/tests/test_gitlabobject.py b/gitlab/tests/test_gitlabobject.py
index f7fd187..844ba9e 100644
--- a/gitlab/tests/test_gitlabobject.py
+++ b/gitlab/tests/test_gitlabobject.py
@@ -34,7 +34,7 @@ from httmock import urlmatch # noqa
from gitlab import * # noqa
-@urlmatch(scheme="http", netloc="localhost", path="/api/v3/projects/1",
+@urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects/1",
method="get")
def resp_get_project(url, request):
headers = {'content-type': 'application/json'}
@@ -42,7 +42,7 @@ def resp_get_project(url, request):
return response(200, content, headers, None, 5, request)
-@urlmatch(scheme="http", netloc="localhost", path="/api/v3/projects",
+@urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects",
method="get")
def resp_list_project(url, request):
headers = {'content-type': 'application/json'}
@@ -50,7 +50,7 @@ def resp_list_project(url, request):
return response(200, content, headers, None, 5, request)
-@urlmatch(scheme="http", netloc="localhost", path="/api/v3/issues/1",
+@urlmatch(scheme="http", netloc="localhost", path="/api/v4/issues/1",
method="get")
def resp_get_issue(url, request):
headers = {'content-type': 'application/json'}
@@ -58,7 +58,7 @@ def resp_get_issue(url, request):
return response(200, content, headers, None, 5, request)
-@urlmatch(scheme="http", netloc="localhost", path="/api/v3/users/1",
+@urlmatch(scheme="http", netloc="localhost", path="/api/v4/users/1",
method="put")
def resp_update_user(url, request):
headers = {'content-type': 'application/json'}
@@ -67,7 +67,7 @@ def resp_update_user(url, request):
return response(200, content, headers, None, 5, request)
-@urlmatch(scheme="http", netloc="localhost", path="/api/v3/projects",
+@urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects",
method="post")
def resp_create_project(url, request):
headers = {'content-type': 'application/json'}
@@ -75,7 +75,7 @@ def resp_create_project(url, request):
return response(201, content, headers, None, 5, request)
-@urlmatch(scheme="http", netloc="localhost", path="/api/v3/groups/2/members",
+@urlmatch(scheme="http", netloc="localhost", path="/api/v4/groups/2/members",
method="post")
def resp_create_groupmember(url, request):
headers = {'content-type': 'application/json'}
@@ -84,14 +84,14 @@ def resp_create_groupmember(url, request):
@urlmatch(scheme="http", netloc="localhost",
- path="/api/v3/projects/2/snippets/3", method="get")
+ path="/api/v4/projects/2/snippets/3", method="get")
def resp_get_projectsnippet(url, request):
headers = {'content-type': 'application/json'}
content = '{"title": "test", "id": 3}'.encode("utf-8")
return response(200, content, headers, None, 5, request)
-@urlmatch(scheme="http", netloc="localhost", path="/api/v3/groups/1",
+@urlmatch(scheme="http", netloc="localhost", path="/api/v4/groups/1",
method="delete")
def resp_delete_group(url, request):
headers = {'content-type': 'application/json'}
@@ -100,7 +100,7 @@ def resp_delete_group(url, request):
@urlmatch(scheme="http", netloc="localhost",
- path="/api/v3/groups/2/projects/3",
+ path="/api/v4/groups/2/projects/3",
method="post")
def resp_transfer_project(url, request):
headers = {'content-type': 'application/json'}
@@ -109,7 +109,7 @@ def resp_transfer_project(url, request):
@urlmatch(scheme="http", netloc="localhost",
- path="/api/v3/groups/2/projects/3",
+ path="/api/v4/groups/2/projects/3",
method="post")
def resp_transfer_project_fail(url, request):
headers = {'content-type': 'application/json'}
@@ -118,7 +118,7 @@ def resp_transfer_project_fail(url, request):
@urlmatch(scheme="http", netloc="localhost",
- path="/api/v3/projects/2/repository/branches/branchname/protect",
+ path="/api/v4/projects/2/repository/branches/branchname/protect",
method="put")
def resp_protect_branch(url, request):
headers = {'content-type': 'application/json'}
@@ -127,7 +127,7 @@ def resp_protect_branch(url, request):
@urlmatch(scheme="http", netloc="localhost",
- path="/api/v3/projects/2/repository/branches/branchname/unprotect",
+ path="/api/v4/projects/2/repository/branches/branchname/unprotect",
method="put")
def resp_unprotect_branch(url, request):
headers = {'content-type': 'application/json'}
@@ -136,7 +136,7 @@ def resp_unprotect_branch(url, request):
@urlmatch(scheme="http", netloc="localhost",
- path="/api/v3/projects/2/repository/branches/branchname/protect",
+ path="/api/v4/projects/2/repository/branches/branchname/protect",
method="put")
def resp_protect_branch_fail(url, request):
headers = {'content-type': 'application/json'}
@@ -157,7 +157,7 @@ class TestGitlabObject(unittest.TestCase):
data = json.loads(json_str)
self.assertIn("id", data)
self.assertEqual(data["username"], "testname")
- self.assertEqual(data["gitlab"]["url"], "http://localhost/api/v3")
+ self.assertEqual(data["gitlab"]["url"], "http://localhost/api/v4")
def test_pickability(self):
gl_object = CurrentUser(self.gl, data={"username": "testname"})
@@ -381,7 +381,7 @@ class TestProjectCommit(unittest.TestCase):
self.obj = ProjectCommit(self.gl, data={"id": 3, "project_id": 2})
@urlmatch(scheme="http", netloc="localhost",
- path="/api/v3/projects/2/repository/commits/3/diff",
+ path="/api/v4/projects/2/repository/commits/3/diff",
method="get")
def resp_diff(self, url, request):
headers = {'content-type': 'application/json'}
@@ -389,7 +389,7 @@ class TestProjectCommit(unittest.TestCase):
return response(200, content, headers, None, 5, request)
@urlmatch(scheme="http", netloc="localhost",
- path="/api/v3/projects/2/repository/commits/3/diff",
+ path="/api/v4/projects/2/repository/commits/3/diff",
method="get")
def resp_diff_fail(self, url, request):
headers = {'content-type': 'application/json'}
@@ -397,7 +397,7 @@ class TestProjectCommit(unittest.TestCase):
return response(400, content, headers, None, 5, request)
@urlmatch(scheme="http", netloc="localhost",
- path="/api/v3/projects/2/repository/blobs/3",
+ path="/api/v4/projects/2/repository/blobs/3",
method="get")
def resp_blob(self, url, request):
headers = {'content-type': 'application/json'}
@@ -405,7 +405,7 @@ class TestProjectCommit(unittest.TestCase):
return response(200, content, headers, None, 5, request)
@urlmatch(scheme="http", netloc="localhost",
- path="/api/v3/projects/2/repository/blobs/3",
+ path="/api/v4/projects/2/repository/blobs/3",
method="get")
def resp_blob_fail(self, url, request):
headers = {'content-type': 'application/json'}
@@ -440,7 +440,7 @@ class TestProjectSnippet(unittest.TestCase):
self.obj = ProjectSnippet(self.gl, data={"id": 3, "project_id": 2})
@urlmatch(scheme="http", netloc="localhost",
- path="/api/v3/projects/2/snippets/3/raw",
+ path="/api/v4/projects/2/snippets/3/raw",
method="get")
def resp_content(self, url, request):
headers = {'content-type': 'application/json'}
@@ -448,7 +448,7 @@ class TestProjectSnippet(unittest.TestCase):
return response(200, content, headers, None, 5, request)
@urlmatch(scheme="http", netloc="localhost",
- path="/api/v3/projects/2/snippets/3/raw",
+ path="/api/v4/projects/2/snippets/3/raw",
method="get")
def resp_content_fail(self, url, request):
headers = {'content-type': 'application/json'}
@@ -474,7 +474,7 @@ class TestSnippet(unittest.TestCase):
self.obj = Snippet(self.gl, data={"id": 3})
@urlmatch(scheme="http", netloc="localhost",
- path="/api/v3/snippets/3/raw",
+ path="/api/v4/snippets/3/raw",
method="get")
def resp_content(self, url, request):
headers = {'content-type': 'application/json'}
@@ -482,7 +482,7 @@ class TestSnippet(unittest.TestCase):
return response(200, content, headers, None, 5, request)
@urlmatch(scheme="http", netloc="localhost",
- path="/api/v3/snippets/3/raw",
+ path="/api/v4/snippets/3/raw",
method="get")
def resp_content_fail(self, url, request):
headers = {'content-type': 'application/json'}
diff --git a/gitlab/tests/test_manager.py b/gitlab/tests/test_manager.py
index 5cd3130..c6ef299 100644
--- a/gitlab/tests/test_manager.py
+++ b/gitlab/tests/test_manager.py
@@ -52,7 +52,8 @@ class TestGitlabManager(unittest.TestCase):
def setUp(self):
self.gitlab = Gitlab("http://localhost", private_token="private_token",
email="testuser@test.com",
- password="testpassword", ssl_verify=True)
+ password="testpassword", ssl_verify=True,
+ api_version=3)
def test_set_parent_args(self):
@urlmatch(scheme="http", netloc="localhost", path="/api/v3/fake",
diff --git a/gitlab/tests/test_mixins.py b/gitlab/tests/test_mixins.py
index e78c757..c51322a 100644
--- a/gitlab/tests/test_mixins.py
+++ b/gitlab/tests/test_mixins.py
@@ -434,89 +434,3 @@ class TestMixinMethods(unittest.TestCase):
self.assertIsInstance(obj, FakeObject)
self.assertEqual(obj.key, 'foo')
self.assertEqual(obj.value, 'bar')
-
-
-class TestExceptions(unittest.TestCase):
- def setUp(self):
- self.gl = Gitlab("http://localhost", private_token="private_token",
- api_version=4)
-
- def test_get_mixin(self):
- class M(GetMixin, FakeManager):
- pass
-
- m = M(self.gl)
- self.assertRaises(GitlabGetError, m.get, 1)
-
- def test_get_without_id_mixin(self):
- class M(GetWithoutIdMixin, FakeManager):
- pass
-
- m = M(self.gl)
- self.assertRaises(GitlabGetError, m.get)
-
- def test_list_mixin(self):
- class M(ListMixin, FakeManager):
- pass
-
- m = M(self.gl)
- self.assertRaises(GitlabListError, m.list)
-
- def test_get_from_list_mixin(self):
- class M(GetFromListMixin, FakeManager):
- pass
-
- m = M(self.gl)
- self.assertRaises(GitlabListError, m.list)
- self.assertRaises(GitlabGetError, m.get, 1)
-
- def test_create_mixin(self):
- class M(CreateMixin, FakeManager):
- pass
-
- m = M(self.gl)
- self.assertRaises(GitlabCreateError, m.create, {})
-
- def test_update_mixin(self):
- class M(UpdateMixin, FakeManager):
- pass
-
- m = M(self.gl)
- self.assertRaises(GitlabUpdateError, m.update, 1, {})
-
- def test_set_mixin(self):
- class M(SetMixin, FakeManager):
- pass
-
- m = M(self.gl)
- self.assertRaises(GitlabSetError, m.set, 'foo', 'bar')
-
- def test_delete_mixin(self):
- class M(DeleteMixin, FakeManager):
- pass
-
- m = M(self.gl)
- self.assertRaises(GitlabDeleteError, m.delete, 1)
-
- def test_object_mixin(self):
- class M(UpdateMixin, DeleteMixin, FakeManager):
- pass
-
- class O(SaveMixin, ObjectDeleteMixin, AccessRequestMixin,
- SubscribableMixin, TodoMixin, TimeTrackingMixin, RESTObject):
- pass
-
- mgr = M(self.gl)
- obj = O(mgr, {'id': 42, 'foo': 'bar'})
- obj.foo = 'baz'
- self.assertRaises(GitlabUpdateError, obj.save)
- self.assertRaises(GitlabDeleteError, obj.delete)
- self.assertRaises(GitlabUpdateError, obj.approve)
- self.assertRaises(GitlabSubscribeError, obj.subscribe)
- self.assertRaises(GitlabUnsubscribeError, obj.unsubscribe)
- self.assertRaises(GitlabTodoError, obj.todo)
- self.assertRaises(GitlabTimeTrackingError, obj.time_stats)
- self.assertRaises(GitlabTimeTrackingError, obj.time_estimate, '1d')
- self.assertRaises(GitlabTimeTrackingError, obj.reset_time_estimate)
- self.assertRaises(GitlabTimeTrackingError, obj.add_spent_time, '1d')
- self.assertRaises(GitlabTimeTrackingError, obj.reset_spent_time)
diff --git a/gitlab/v3/objects.py b/gitlab/v3/objects.py
index 014714e..0db9dfd 100644
--- a/gitlab/v3/objects.py
+++ b/gitlab/v3/objects.py
@@ -934,7 +934,7 @@ class ProjectIssue(GitlabObject):
{'project_id': self.project_id, 'issue_id': self.id})
r = self.gitlab._raw_post(url, **kwargs)
- raise_error_from_response(r, GitlabSubscribeError)
+ raise_error_from_response(r, GitlabSubscribeError, 201)
self._set_from_dict(r.json())
def unsubscribe(self, **kwargs):
@@ -1496,18 +1496,6 @@ class ProjectFileManager(BaseManager):
obj_cls = ProjectFile
-class ProjectPipelineSchedule(GitlabObject):
- _url = '/projects/%(project_id)s/pipeline_schedules'
- _create_url = '/projects/%(project_id)s/pipeline_schedules'
-
- requiredUrlAttrs = ['project_id']
- requiredCreateAttrs = ['description', 'ref', 'cron']
-
-
-class ProjectPipelineSchedulesManager(BaseManager):
- obj_cls = ProjectPipelineSchedule
-
-
class ProjectPipeline(GitlabObject):
_url = '/projects/%(project_id)s/pipelines'
_create_url = '/projects/%(project_id)s/pipeline'
@@ -1815,7 +1803,6 @@ class Project(GitlabObject):
('notificationsettings', 'ProjectNotificationSettingsManager',
[('project_id', 'id')]),
('pipelines', 'ProjectPipelineManager', [('project_id', 'id')]),
- ('pipeline_schedules', 'ProjectPipelineSchedulesManager', [('project_id', 'id')]),
('runners', 'ProjectRunnerManager', [('project_id', 'id')]),
('services', 'ProjectServiceManager', [('project_id', 'id')]),
('snippets', 'ProjectSnippetManager', [('project_id', 'id')]),
diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py
index 17e987c..aff5e17 100644
--- a/gitlab/v4/objects.py
+++ b/gitlab/v4/objects.py
@@ -112,6 +112,17 @@ class SidekiqManager(RESTManager):
return self.gitlab.http_get('/sidekiq/compound_metrics', **kwargs)
+class Event(RESTObject):
+ _id_attr = None
+ _short_print_attr = 'target_title'
+
+
+class EventManager(ListMixin, RESTManager):
+ _path = '/events'
+ _obj_cls = Event
+ _list_filters = ('action', 'target_type', 'before', 'after', 'sort')
+
+
class UserActivities(RESTObject):
_id_attr = 'username'
@@ -143,6 +154,16 @@ class UserEmailManager(RetrieveMixin, CreateMixin, DeleteMixin, RESTManager):
_create_attrs = (('email', ), tuple())
+class UserEvent(Event):
+ pass
+
+
+class UserEventManager(EventManager):
+ _path = '/users/%(user_id)s/events'
+ _obj_cls = UserEvent
+ _from_parent_attrs = {'user_id': 'id'}
+
+
class UserGPGKey(ObjectDeleteMixin, RESTObject):
pass
@@ -181,7 +202,7 @@ class UserProject(RESTObject):
pass
-class UserProjectManager(CreateMixin, RESTManager):
+class UserProjectManager(ListMixin, CreateMixin, RESTManager):
_path = '/projects/user/%(user_id)s'
_obj_cls = UserProject
_from_parent_attrs = {'user_id': 'id'}
@@ -192,6 +213,31 @@ class UserProjectManager(CreateMixin, RESTManager):
'public', 'visibility', 'description', 'builds_enabled',
'public_builds', 'import_url', 'only_allow_merge_if_build_succeeds')
)
+ _list_filters = ('archived', 'visibility', 'order_by', 'sort', 'search',
+ 'simple', 'owned', 'membership', 'starred', 'statistics',
+ 'with_issues_enabled', 'with_merge_requests_enabled')
+
+ def list(self, **kwargs):
+ """Retrieve a list of objects.
+
+ Args:
+ all (bool): If True, return all the items, without pagination
+ per_page (int): Number of items to retrieve per request
+ page (int): ID of the page to return (starts with page 1)
+ as_list (bool): If set to False and no pagination option is
+ defined, return a generator instead of a list
+ **kwargs: Extra options to send to the Gitlab server (e.g. sudo)
+
+ Returns:
+ list: The list of objects, or a generator if `as_list` is False
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabListError: If the server cannot perform the request
+ """
+
+ path = '/users/%s/projects' % self._parent.id
+ return ListMixin.list(self, path=path, **kwargs)
class User(SaveMixin, ObjectDeleteMixin, RESTObject):
@@ -199,6 +245,7 @@ class User(SaveMixin, ObjectDeleteMixin, RESTObject):
_managers = (
('customattributes', 'UserCustomAttributeManager'),
('emails', 'UserEmailManager'),
+ ('events', 'UserEventManager'),
('gpgkeys', 'UserGPGKeyManager'),
('impersonationtokens', 'UserImpersonationTokenManager'),
('keys', 'UserKeyManager'),
@@ -253,7 +300,7 @@ class UserManager(CRUDMixin, RESTManager):
_obj_cls = User
_list_filters = ('active', 'blocked', 'username', 'extern_uid', 'provider',
- 'external', 'search')
+ 'external', 'search', 'custom_attributes')
_create_attrs = (
tuple(),
('email', 'username', 'name', 'password', 'reset_password', 'skype',
@@ -400,6 +447,38 @@ class DockerfileManager(RetrieveMixin, RESTManager):
_obj_cls = Dockerfile
+class Feature(RESTObject):
+ _id_attr = 'name'
+
+
+class FeatureManager(ListMixin, RESTManager):
+ _path = '/features/'
+ _obj_cls = Feature
+
+ @exc.on_http_error(exc.GitlabSetError)
+ def set(self, name, value, feature_group=None, user=None, **kwargs):
+ """Create or update the object.
+
+ Args:
+ name (str): The value to set for the object
+ value (bool/int): The value to set for the object
+ feature_group (str): A feature group name
+ user (str): A GitLab username
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabSetError: If an error occured
+
+ Returns:
+ obj: The created/updated attribute
+ """
+ path = '%s/%s' % (self.path, name.replace('/', '%2F'))
+ data = {'value': value, 'feature_group': feature_group, 'user': user}
+ server_data = self.gitlab.http_post(path, post_data=data, **kwargs)
+ return self._obj_cls(self, server_data)
+
+
class Gitignore(RESTObject):
_id_attr = 'name'
@@ -429,6 +508,17 @@ class GroupAccessRequestManager(GetFromListMixin, CreateMixin, DeleteMixin,
_from_parent_attrs = {'group_id': 'id'}
+class GroupCustomAttribute(ObjectDeleteMixin, RESTObject):
+ _id_attr = 'key'
+
+
+class GroupCustomAttributeManager(RetrieveMixin, SetMixin, DeleteMixin,
+ RESTManager):
+ _path = '/groups/%(group_id)s/custom_attributes'
+ _obj_cls = GroupCustomAttribute
+ _from_parent_attrs = {'group_id': 'id'}
+
+
class GroupIssue(RESTObject):
pass
@@ -470,6 +560,11 @@ class GroupMilestone(SaveMixin, ObjectDeleteMixin, RESTObject):
"""List issues related to this milestone.
Args:
+ all (bool): If True, return all the items, without pagination
+ per_page (int): Number of items to retrieve per request
+ page (int): ID of the page to return (starts with page 1)
+ as_list (bool): If set to False and no pagination option is
+ defined, return a generator instead of a list
**kwargs: Extra options to send to the server (e.g. sudo)
Raises:
@@ -494,6 +589,11 @@ class GroupMilestone(SaveMixin, ObjectDeleteMixin, RESTObject):
"""List the merge requests related to this milestone.
Args:
+ all (bool): If True, return all the items, without pagination
+ per_page (int): Number of items to retrieve per request
+ page (int): ID of the page to return (starts with page 1)
+ as_list (bool): If set to False and no pagination option is
+ defined, return a generator instead of a list
**kwargs: Extra options to send to the server (e.g. sudo)
Raises:
@@ -544,6 +644,18 @@ class GroupProjectManager(GetFromListMixin, RESTManager):
'ci_enabled_first')
+class GroupSubgroup(RESTObject):
+ pass
+
+
+class GroupSubgroupManager(GetFromListMixin, RESTManager):
+ _path = '/groups/%(group_id)s/subgroups'
+ _obj_cls = GroupSubgroup
+ _from_parent_attrs = {'group_id': 'id'}
+ _list_filters = ('skip_groups', 'all_available', 'search', 'order_by',
+ 'sort', 'statistics', 'owned')
+
+
class GroupVariable(SaveMixin, ObjectDeleteMixin, RESTObject):
_id_attr = 'key'
@@ -560,11 +672,13 @@ class Group(SaveMixin, ObjectDeleteMixin, RESTObject):
_short_print_attr = 'name'
_managers = (
('accessrequests', 'GroupAccessRequestManager'),
+ ('customattributes', 'GroupCustomAttributeManager'),
+ ('issues', 'GroupIssueManager'),
('members', 'GroupMemberManager'),
('milestones', 'GroupMilestoneManager'),
('notificationsettings', 'GroupNotificationSettingsManager'),
('projects', 'GroupProjectManager'),
- ('issues', 'GroupIssueManager'),
+ ('subgroups', 'GroupSubgroupManager'),
('variables', 'GroupVariableManager'),
)
@@ -588,6 +702,8 @@ class Group(SaveMixin, ObjectDeleteMixin, RESTObject):
class GroupManager(CRUDMixin, RESTManager):
_path = '/groups'
_obj_cls = Group
+ _list_filters = ('skip_groups', 'all_available', 'search', 'order_by',
+ 'sort', 'statistics', 'owned', 'custom_attributes')
_create_attrs = (
('name', 'path'),
('description', 'visibility', 'parent_id', 'lfs_enabled',
@@ -698,6 +814,15 @@ class NamespaceManager(GetFromListMixin, RESTManager):
_list_filters = ('search', )
+class PagesDomain(RESTObject):
+ _id_attr = 'domain'
+
+
+class PagesDomainManager(ListMixin, RESTManager):
+ _path = '/pages/domains'
+ _obj_cls = PagesDomain
+
+
class ProjectBoardList(SaveMixin, ObjectDeleteMixin, RESTObject):
pass
@@ -773,6 +898,17 @@ class ProjectBranchManager(NoUpdateMixin, RESTManager):
_create_attrs = (('branch', 'ref'), tuple())
+class ProjectCustomAttribute(ObjectDeleteMixin, RESTObject):
+ _id_attr = 'key'
+
+
+class ProjectCustomAttributeManager(RetrieveMixin, SetMixin, DeleteMixin,
+ RESTManager):
+ _path = '/projects/%(project_id)s/custom_attributes'
+ _obj_cls = ProjectCustomAttribute
+ _from_parent_attrs = {'project_id': 'id'}
+
+
class ProjectJob(RESTObject):
@cli.register_custom_action('ProjectJob')
@exc.on_http_error(exc.GitlabJobCancelError)
@@ -1047,12 +1183,11 @@ class ProjectKeyManager(NoUpdateMixin, RESTManager):
self.gitlab.http_post(path, **kwargs)
-class ProjectEvent(RESTObject):
- _id_attr = None
- _short_print_attr = 'target_title'
+class ProjectEvent(Event):
+ pass
-class ProjectEventManager(ListMixin, RESTManager):
+class ProjectEventManager(EventManager):
_path = '/projects/%(project_id)s/events'
_obj_cls = ProjectEvent
_from_parent_attrs = {'project_id': 'id'}
@@ -1091,10 +1226,35 @@ class ProjectHookManager(CRUDMixin, RESTManager):
)
-class ProjectIssueNote(SaveMixin, ObjectDeleteMixin, RESTObject):
+class ProjectIssueAwardEmoji(ObjectDeleteMixin, RESTObject):
+ pass
+
+
+class ProjectIssueAwardEmojiManager(NoUpdateMixin, RESTManager):
+ _path = '/projects/%(project_id)s/issues/%(issue_iid)s/award_emoji'
+ _obj_cls = ProjectIssueAwardEmoji
+ _from_parent_attrs = {'project_id': 'project_id', 'issue_iid': 'iid'}
+ _create_attrs = (('name', ), tuple())
+
+
+class ProjectIssueNoteAwardEmoji(ObjectDeleteMixin, RESTObject):
pass
+class ProjectIssueNoteAwardEmojiManager(NoUpdateMixin, RESTManager):
+ _path = ('/projects/%(project_id)s/issues/%(issue_iid)s'
+ '/notes/%(note_id)s/award_emoji')
+ _obj_cls = ProjectIssueNoteAwardEmoji
+ _from_parent_attrs = {'project_id': 'project_id',
+ 'issue_iid': 'issue_iid',
+ 'note_id': 'id'}
+ _create_attrs = (('name', ), tuple())
+
+
+class ProjectIssueNote(SaveMixin, ObjectDeleteMixin, RESTObject):
+ _managers = (('awardemojis', 'ProjectIssueNoteAwardEmojiManager'),)
+
+
class ProjectIssueNoteManager(CRUDMixin, RESTManager):
_path = '/projects/%(project_id)s/issues/%(issue_iid)s/notes'
_obj_cls = ProjectIssueNote
@@ -1107,7 +1267,10 @@ class ProjectIssue(SubscribableMixin, TodoMixin, TimeTrackingMixin, SaveMixin,
ObjectDeleteMixin, RESTObject):
_short_print_attr = 'title'
_id_attr = 'iid'
- _managers = (('notes', 'ProjectIssueNoteManager'), )
+ _managers = (
+ ('notes', 'ProjectIssueNoteManager'),
+ ('awardemojis', 'ProjectIssueAwardEmojiManager'),
+ )
@cli.register_custom_action('ProjectIssue')
@exc.on_http_error(exc.GitlabUpdateError)
@@ -1196,6 +1359,18 @@ class ProjectNotificationSettingsManager(NotificationSettingsManager):
_from_parent_attrs = {'project_id': 'id'}
+class ProjectPagesDomain(SaveMixin, ObjectDeleteMixin, RESTObject):
+ _id_attr = 'domain'
+
+
+class ProjectPagesDomainManager(CRUDMixin, RESTManager):
+ _path = '/projects/%(project_id)s/pages/domains'
+ _obj_cls = ProjectPagesDomain
+ _from_parent_attrs = {'project_id': 'id'}
+ _create_attrs = (('domain', ), ('certificate', 'key'))
+ _update_attrs = (tuple(), ('certificate', 'key'))
+
+
class ProjectTag(ObjectDeleteMixin, RESTObject):
_id_attr = 'name'
_short_print_attr = 'name'
@@ -1243,6 +1418,17 @@ class ProjectTagManager(NoUpdateMixin, RESTManager):
_create_attrs = (('tag_name', 'ref'), ('message',))
+class ProjectMergeRequestAwardEmoji(ObjectDeleteMixin, RESTObject):
+ pass
+
+
+class ProjectMergeRequestAwardEmojiManager(NoUpdateMixin, RESTManager):
+ _path = '/projects/%(project_id)s/merge_requests/%(mr_iid)s/award_emoji'
+ _obj_cls = ProjectMergeRequestAwardEmoji
+ _from_parent_attrs = {'project_id': 'project_id', 'mr_iid': 'iid'}
+ _create_attrs = (('name', ), tuple())
+
+
class ProjectMergeRequestDiff(RESTObject):
pass
@@ -1253,10 +1439,24 @@ class ProjectMergeRequestDiffManager(RetrieveMixin, RESTManager):
_from_parent_attrs = {'project_id': 'project_id', 'mr_iid': 'iid'}
-class ProjectMergeRequestNote(SaveMixin, ObjectDeleteMixin, RESTObject):
+class ProjectMergeRequestNoteAwardEmoji(ObjectDeleteMixin, RESTObject):
pass
+class ProjectMergeRequestNoteAwardEmojiManager(NoUpdateMixin, RESTManager):
+ _path = ('/projects/%(project_id)s/merge_requests/%(mr_iid)s'
+ '/notes/%(note_id)s/award_emoji')
+ _obj_cls = ProjectMergeRequestNoteAwardEmoji
+ _from_parent_attrs = {'project_id': 'project_id',
+ 'mr_iid': 'issue_iid',
+ 'note_id': 'id'}
+ _create_attrs = (('name', ), tuple())
+
+
+class ProjectMergeRequestNote(SaveMixin, ObjectDeleteMixin, RESTObject):
+ _managers = (('awardemojis', 'ProjectMergeRequestNoteAwardEmojiManager'),)
+
+
class ProjectMergeRequestNoteManager(CRUDMixin, RESTManager):
_path = '/projects/%(project_id)s/merge_requests/%(mr_iid)s/notes'
_obj_cls = ProjectMergeRequestNote
@@ -1270,8 +1470,9 @@ class ProjectMergeRequest(SubscribableMixin, TodoMixin, TimeTrackingMixin,
_id_attr = 'iid'
_managers = (
+ ('awardemojis', 'ProjectMergeRequestAwardEmojiManager'),
+ ('diffs', 'ProjectMergeRequestDiffManager'),
('notes', 'ProjectMergeRequestNoteManager'),
- ('diffs', 'ProjectMergeRequestDiffManager')
)
@cli.register_custom_action('ProjectMergeRequest')
@@ -1299,6 +1500,11 @@ class ProjectMergeRequest(SubscribableMixin, TodoMixin, TimeTrackingMixin,
"""List issues that will close on merge."
Args:
+ all (bool): If True, return all the items, without pagination
+ per_page (int): Number of items to retrieve per request
+ page (int): ID of the page to return (starts with page 1)
+ as_list (bool): If set to False and no pagination option is
+ defined, return a generator instead of a list
**kwargs: Extra options to send to the server (e.g. sudo)
Raises:
@@ -1321,6 +1527,11 @@ class ProjectMergeRequest(SubscribableMixin, TodoMixin, TimeTrackingMixin,
"""List the merge request commits.
Args:
+ all (bool): If True, return all the items, without pagination
+ per_page (int): Number of items to retrieve per request
+ page (int): ID of the page to return (starts with page 1)
+ as_list (bool): If set to False and no pagination option is
+ defined, return a generator instead of a list
**kwargs: Extra options to send to the server (e.g. sudo)
Raises:
@@ -1392,6 +1603,30 @@ class ProjectMergeRequest(SubscribableMixin, TodoMixin, TimeTrackingMixin,
**kwargs)
self._update_attrs(server_data)
+ @cli.register_custom_action('ProjectMergeRequest')
+ @exc.on_http_error(exc.GitlabListError)
+ def participants(self, **kwargs):
+ """List the merge request participants.
+
+ Args:
+ all (bool): If True, return all the items, without pagination
+ per_page (int): Number of items to retrieve per request
+ page (int): ID of the page to return (starts with page 1)
+ as_list (bool): If set to False and no pagination option is
+ defined, return a generator instead of a list
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabListError: If the list could not be retrieved
+
+ Returns:
+ RESTObjectList: The list of participants
+ """
+
+ path = '%s/%s/participants' % (self.manager.path, self.get_id())
+ return self.manager.gitlab.http_get(path, **kwargs)
+
class ProjectMergeRequestManager(CRUDMixin, RESTManager):
_path = '/projects/%(project_id)s/merge_requests'
@@ -1423,6 +1658,11 @@ class ProjectMilestone(SaveMixin, ObjectDeleteMixin, RESTObject):
"""List issues related to this milestone.
Args:
+ all (bool): If True, return all the items, without pagination
+ per_page (int): Number of items to retrieve per request
+ page (int): ID of the page to return (starts with page 1)
+ as_list (bool): If set to False and no pagination option is
+ defined, return a generator instead of a list
**kwargs: Extra options to send to the server (e.g. sudo)
Raises:
@@ -1447,6 +1687,11 @@ class ProjectMilestone(SaveMixin, ObjectDeleteMixin, RESTObject):
"""List the merge requests related to this milestone.
Args:
+ all (bool): If True, return all the items, without pagination
+ per_page (int): Number of items to retrieve per request
+ page (int): ID of the page to return (starts with page 1)
+ as_list (bool): If set to False and no pagination option is
+ defined, return a generator instead of a list
**kwargs: Extra options to send to the server (e.g. sudo)
Raises:
@@ -1625,9 +1870,10 @@ class ProjectFileManager(GetMixin, CreateMixin, UpdateMixin, DeleteMixin,
"""
self._check_missing_create_attrs(data)
- file_path = data.pop('file_path').replace('/', '%2F')
+ new_data = data.copy()
+ file_path = new_data.pop('file_path').replace('/', '%2F')
path = '%s/%s' % (self.path, file_path)
- server_data = self.gitlab.http_post(path, post_data=data, **kwargs)
+ server_data = self.gitlab.http_post(path, post_data=new_data, **kwargs)
return self._obj_cls(self, server_data)
@exc.on_http_error(exc.GitlabUpdateError)
@@ -1706,22 +1952,8 @@ class ProjectFileManager(GetMixin, CreateMixin, UpdateMixin, DeleteMixin,
return utils.response_content(result, streamed, action, chunk_size)
-class ProjectPipelineJob(ProjectJob):
- pass
-
-
-class ProjectPipelineJobsManager(ListMixin, RESTManager):
- _path = '/projects/%(project_id)s/pipelines/%(pipeline_id)s/jobs'
- _obj_cls = ProjectPipelineJob
- _from_parent_attrs = {'project_id': 'project_id',
- 'pipeline_id' : 'id'}
- _list_filters = ('scope',)
-
-
class ProjectPipeline(RESTObject):
- _managers = (
- ('jobs', 'ProjectPipelineJobsManager'),
- )
+ _managers = (('jobs', 'ProjectPipelineJobManager'), )
@cli.register_custom_action('ProjectPipeline')
@exc.on_http_error(exc.GitlabPipelineCancelError)
@@ -1784,65 +2016,79 @@ class ProjectPipelineScheduleVariable(SaveMixin, ObjectDeleteMixin, RESTObject):
_id_attr = 'key'
-class ProjectPipelineScheduleVariableManager(CRUDMixin, RESTManager):
- _path = '/projects/%(project_id)s/pipeline_schedules/%(pipeline_schedule_id)s/variables'
+class ProjectPipelineScheduleVariableManager(CreateMixin, UpdateMixin,
+ DeleteMixin, RESTManager):
+ _path = ('/projects/%(project_id)s/pipeline_schedules/'
+ '%(pipeline_schedule_id)s/variables')
_obj_cls = ProjectPipelineScheduleVariable
_from_parent_attrs = {'project_id': 'project_id',
'pipeline_schedule_id' : 'id'}
- _create_attrs = (('pipeline_schedule_id', 'key', 'value'), tuple())
_create_attrs = (('key', 'value'), tuple())
+ _update_attrs = (('key', 'value'), tuple())
- def list(self):
- array = []
- if 'variables' in self._parent._attrs:
- for variable in self._parent._attrs['variables']:
- schedule_variable = self._obj_cls(self, variable)
- array.append(schedule_variable)
- else:
- obj = self._parent.manager.get(self._parent.id)
- for variable in obj._attrs['variables']:
- schedule_variable = self._obj_cls(self, variable)
- array.append(schedule_variable)
- return array
+class ProjectPipelineSchedule(SaveMixin, ObjectDeleteMixin, RESTObject):
+ _managers = (('variables', 'ProjectPipelineScheduleVariableManager'),)
+ @cli.register_custom_action('ProjectPipelineSchedule')
+ @exc.on_http_error(exc.GitlabOwnershipError)
+ def take_ownership(self, **kwargs):
+ """Update the owner of a pipeline schedule.
-class ProjectPipelineSchedule(RESTObject):
- _managers = (
- ('variables', 'ProjectPipelineScheduleVariableManager'),
- )
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabOwnershipError: If the request failed
+ """
+ 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 ProjectPipelineSchedulesManager(RetrieveMixin, CreateMixin, RESTManager):
+
+class ProjectPipelineScheduleManager(CRUDMixin, RESTManager):
_path = '/projects/%(project_id)s/pipeline_schedules'
_obj_cls = ProjectPipelineSchedule
_from_parent_attrs = {'project_id': 'id'}
_create_attrs = (('description', 'ref', 'cron'),
('cron_timezone', 'active'))
+ _update_attrs = (tuple(),
+ ('description', 'ref', 'cron', 'cron_timezone', 'active'))
- def create(self, data, **kwargs):
- """Creates a new object.
- Args:
- data (dict): Parameters to send to the server to create the
- resource
- **kwargs: Extra options to send to the server (e.g. sudo)
+class ProjectSnippetNote(SaveMixin, ObjectDeleteMixin, RESTObject):
+ pass
- Raises:
- GitlabAuthenticationError: If authentication is not correct
- GitlabCreateError: If the server cannot perform the request
- Returns:
- RESTObject: A new instance of the managed object class build with
- the data sent by the server
- """
- return CreateMixin.create(self, data, path=self.path, **kwargs)
+class ProjectPipelineJob(ProjectJob):
+ pass
-class ProjectSnippetNote(SaveMixin, ObjectDeleteMixin, RESTObject):
+class ProjectPipelineJobManager(GetFromListMixin, RESTManager):
+ _path = '/projects/%(project_id)s/pipelines/%(pipeline_id)s/jobs'
+ _obj_cls = ProjectPipelineJob
+ _from_parent_attrs = {'project_id': 'project_id', 'pipeline_id': 'id'}
+
+
+class ProjectSnippetNoteAwardEmoji(ObjectDeleteMixin, RESTObject):
pass
+class ProjectSnippetNoteAwardEmojiManager(NoUpdateMixin, RESTManager):
+ _path = ('/projects/%(project_id)s/snippets/%(snippet_id)s'
+ '/notes/%(note_id)s/award_emoji')
+ _obj_cls = ProjectSnippetNoteAwardEmoji
+ _from_parent_attrs = {'project_id': 'project_id',
+ 'snippet_id': 'snippet_id',
+ 'note_id': 'id'}
+ _create_attrs = (('name', ), tuple())
+
+
+class ProjectSnippetNote(SaveMixin, ObjectDeleteMixin, RESTObject):
+ _managers = (('awardemojis', 'ProjectSnippetNoteAwardEmojiManager'),)
+
+
class ProjectSnippetNoteManager(CRUDMixin, RESTManager):
_path = '/projects/%(project_id)s/snippets/%(snippet_id)s/notes'
_obj_cls = ProjectSnippetNote
@@ -1852,10 +2098,24 @@ class ProjectSnippetNoteManager(CRUDMixin, RESTManager):
_update_attrs = (('body', ), tuple())
+class ProjectSnippetAwardEmoji(ObjectDeleteMixin, RESTObject):
+ pass
+
+
+class ProjectSnippetAwardEmojiManager(NoUpdateMixin, RESTManager):
+ _path = '/projects/%(project_id)s/snippets/%(snippet_id)s/award_emoji'
+ _obj_cls = ProjectSnippetAwardEmoji
+ _from_parent_attrs = {'project_id': 'project_id', 'snippet_id': 'id'}
+ _create_attrs = (('name', ), tuple())
+
+
class ProjectSnippet(SaveMixin, ObjectDeleteMixin, RESTObject):
_url = '/projects/%(project_id)s/snippets'
_short_print_attr = 'title'
- _managers = (('notes', 'ProjectSnippetNoteManager'), )
+ _managers = (
+ ('awardemojis', 'ProjectSnippetAwardEmojiManager'),
+ ('notes', 'ProjectSnippetNoteManager'),
+ )
@cli.register_custom_action('ProjectSnippet')
@exc.on_http_error(exc.GitlabGetError)
@@ -1895,8 +2155,17 @@ class ProjectSnippetManager(CRUDMixin, RESTManager):
class ProjectTrigger(SaveMixin, ObjectDeleteMixin, RESTObject):
@cli.register_custom_action('ProjectTrigger')
+ @exc.on_http_error(exc.GitlabOwnershipError)
def take_ownership(self, **kwargs):
- """Update the owner of a trigger."""
+ """Update the owner of a trigger.
+
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabOwnershipError: If the request failed
+ """
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)
@@ -2094,6 +2363,7 @@ class Project(SaveMixin, ObjectDeleteMixin, RESTObject):
('branches', 'ProjectBranchManager'),
('jobs', 'ProjectJobManager'),
('commits', 'ProjectCommitManager'),
+ ('customattributes', 'ProjectCustomAttributeManager'),
('deployments', 'ProjectDeploymentManager'),
('environments', 'ProjectEnvironmentManager'),
('events', 'ProjectEventManager'),
@@ -2108,9 +2378,10 @@ class Project(SaveMixin, ObjectDeleteMixin, RESTObject):
('milestones', 'ProjectMilestoneManager'),
('notes', 'ProjectNoteManager'),
('notificationsettings', 'ProjectNotificationSettingsManager'),
+ ('pagesdomains', 'ProjectPagesDomainManager'),
('pipelines', 'ProjectPipelineManager'),
('protectedbranches', 'ProjectProtectedBranchManager'),
- ('pipeline_schedules', 'ProjectPipelineSchedulesManager'),
+ ('pipelineschedules', 'ProjectPipelineScheduleManager'),
('runners', 'ProjectRunnerManager'),
('services', 'ProjectServiceManager'),
('snippets', 'ProjectSnippetManager'),
@@ -2129,6 +2400,11 @@ class Project(SaveMixin, ObjectDeleteMixin, RESTObject):
Args:
path (str): Path of the top folder (/ by default)
ref (str): Reference to a commit or branch
+ all (bool): If True, return all the items, without pagination
+ per_page (int): Number of items to retrieve per request
+ page (int): ID of the page to return (starts with page 1)
+ as_list (bool): If set to False and no pagination option is
+ defined, return a generator instead of a list
**kwargs: Extra options to send to the server (e.g. sudo)
Raises:
@@ -2223,6 +2499,11 @@ class Project(SaveMixin, ObjectDeleteMixin, RESTObject):
"""Return a list of contributors for the project.
Args:
+ all (bool): If True, return all the items, without pagination
+ per_page (int): Number of items to retrieve per request
+ page (int): ID of the page to return (starts with page 1)
+ as_list (bool): If set to False and no pagination option is
+ defined, return a generator instead of a list
**kwargs: Extra options to send to the server (e.g. sudo)
Raises:
@@ -2499,7 +2780,8 @@ class ProjectManager(CRUDMixin, RESTManager):
)
_list_filters = ('search', 'owned', 'starred', 'archived', 'visibility',
'order_by', 'sort', 'simple', 'membership', 'statistics',
- 'with_issues_enabled', 'with_merge_requests_enabled')
+ 'with_issues_enabled', 'with_merge_requests_enabled',
+ 'custom_attributes')
class Runner(SaveMixin, ObjectDeleteMixin, RESTObject):
diff --git a/setup.py b/setup.py
index 25a5693..e46a355 100644
--- a/setup.py
+++ b/setup.py
@@ -34,6 +34,13 @@ setup(name='python-gitlab',
'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)',
'Natural Language :: English',
'Operating System :: POSIX',
- 'Operating System :: Microsoft :: Windows'
+ 'Operating System :: Microsoft :: Windows',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.4',
+ 'Programming Language :: Python :: 3.5',
+ 'Programming Language :: Python :: 3.6',
]
)
diff --git a/tools/build_test_env.sh b/tools/build_test_env.sh
index 31651b3..7e149f6 100755
--- a/tools/build_test_env.sh
+++ b/tools/build_test_env.sh
@@ -94,6 +94,21 @@ testcase() {
OK
}
+if [ -z "$NOVENV" ]; then
+ log "Creating Python virtualenv..."
+ try "$VENV_CMD" "$VENV"
+ . "$VENV"/bin/activate || fatal "failed to activate Python virtual environment"
+
+ log "Installing dependencies into virtualenv..."
+ try pip install -rrequirements.txt
+
+ log "Installing into virtualenv..."
+ try pip install -e .
+
+ # to run generate_token.py
+ pip install bs4 lxml
+fi
+
log "Waiting for gitlab to come online... "
I=0
while :; do
@@ -107,23 +122,7 @@ while :; do
done
# Get the token
-log "Getting GitLab token..."
-I=0
-while :; do
- sleep 1
- TOKEN_JSON=$(
- try curl -s http://localhost:8080/api/v3/session \
- -X POST \
- --data "login=$LOGIN&password=$PASSWORD"
- ) >/dev/null 2>&1 || true
- TOKEN=$(
- pecho "${TOKEN_JSON}" |
- try python -c \
- 'import sys, json; print(json.load(sys.stdin)["private_token"])'
- ) >/dev/null 2>&1 && break
- I=$((I+1))
- [ "$I" -lt 20 ] || fatal "timed out"
-done
+TOKEN=$($(dirname $0)/generate_token.py)
cat > $CONFIG << EOF
[global]
@@ -139,18 +138,6 @@ EOF
log "Config file content ($CONFIG):"
log <$CONFIG
-if [ -z "$NOVENV" ]; then
- log "Creating Python virtualenv..."
- try "$VENV_CMD" "$VENV"
- . "$VENV"/bin/activate || fatal "failed to activate Python virtual environment"
-
- log "Installing dependencies into virtualenv..."
- try pip install -rrequirements.txt
-
- log "Installing into virtualenv..."
- try pip install -e .
-fi
-
log "Pausing to give GitLab some time to finish starting up..."
sleep 30
diff --git a/tools/generate_token.py b/tools/generate_token.py
new file mode 100755
index 0000000..ab14188
--- /dev/null
+++ b/tools/generate_token.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python
+
+import sys
+try:
+ from urllib.parse import urljoin
+except ImportError:
+ from urlparse import urljoin
+
+from bs4 import BeautifulSoup
+import requests
+
+endpoint = "http://localhost:8080"
+root_route = urljoin(endpoint, "/")
+sign_in_route = urljoin(endpoint, "/users/sign_in")
+pat_route = urljoin(endpoint, "/profile/personal_access_tokens")
+
+login = "root"
+password = "5iveL!fe"
+
+
+def find_csrf_token(text):
+ soup = BeautifulSoup(text, "lxml")
+ token = soup.find(attrs={"name": "csrf-token"})
+ param = soup.find(attrs={"name": "csrf-param"})
+ data = {param.get("content"): token.get("content")}
+ return data
+
+
+def obtain_csrf_token():
+ r = requests.get(root_route)
+ token = find_csrf_token(r.text)
+ return token, r.cookies
+
+
+def sign_in(csrf, cookies):
+ data = {
+ "user[login]": login,
+ "user[password]": password,
+ }
+ data.update(csrf)
+ r = requests.post(sign_in_route, data=data, cookies=cookies)
+ token = find_csrf_token(r.text)
+ return token, r.history[0].cookies
+
+
+def obtain_personal_access_token(name, csrf, cookies):
+ data = {
+ "personal_access_token[name]": name,
+ "personal_access_token[scopes][]": ["api", "sudo"],
+ }
+ data.update(csrf)
+ r = requests.post(pat_route, data=data, cookies=cookies)
+ soup = BeautifulSoup(r.text, "lxml")
+ token = soup.find('input', id='created-personal-access-token').get('value')
+ return token
+
+
+def main():
+ csrf1, cookies1 = obtain_csrf_token()
+ csrf2, cookies2 = sign_in(csrf1, cookies1)
+
+ token = obtain_personal_access_token('default', csrf2, cookies2)
+ print(token)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/python_test_v3.py b/tools/python_test_v3.py
index 00faccc..c16bb40 100644
--- a/tools/python_test_v3.py
+++ b/tools/python_test_v3.py
@@ -21,14 +21,8 @@ DEPLOY_KEY = ("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFdRyjJQh+1niBpXqE2I8dzjG"
"rke9IepE7SPBT41C+YtUX4dfDZDmczM1cE0YL/krdUCfuZHMa4ZS2YyNd6slufc"
"vn bar@foo")
-# login/password authentication
-gl = gitlab.Gitlab('http://localhost:8080', email=LOGIN, password=PASSWORD)
-gl.auth()
-token_from_auth = gl.private_token
-
# token authentication from config file
gl = gitlab.Gitlab.from_config(config_files=['/tmp/python-gitlab.cfg'])
-assert(token_from_auth == gl.private_token)
gl.auth()
assert(isinstance(gl.user, gitlab.v3.objects.CurrentUser))
@@ -105,7 +99,7 @@ p_id = gl.groups.search('group2')[0].id
group3 = gl.groups.create({'name': 'group3', 'path': 'group3', 'parent_id': p_id})
assert(len(gl.groups.list()) == 3)
-assert(len(gl.groups.search("1")) == 1)
+assert(len(gl.groups.search("oup1")) == 1)
assert(group3.parent_id == p_id)
group1.members.create({'access_level': gitlab.Group.OWNER_ACCESS,
diff --git a/tools/python_test_v4.py b/tools/python_test_v4.py
index f9ef83a..695722f 100644
--- a/tools/python_test_v4.py
+++ b/tools/python_test_v4.py
@@ -51,14 +51,8 @@ qG2ZdhHHmSK2LaQLFiSprUkikStNU9BqSQ==
-----END PGP PUBLIC KEY BLOCK-----'''
-# login/password authentication
-gl = gitlab.Gitlab('http://localhost:8080', email=LOGIN, password=PASSWORD)
-gl.auth()
-token_from_auth = gl.private_token
-
# token authentication from config file
gl = gitlab.Gitlab.from_config(config_files=['/tmp/python-gitlab.cfg'])
-assert(token_from_auth == gl.private_token)
gl.auth()
assert(isinstance(gl.user, gitlab.v4.objects.CurrentUser))
@@ -98,6 +92,12 @@ assert(new_user.email == user.email)
new_user.block()
new_user.unblock()
+# user projects list
+assert(len(new_user.projects.list()) == 0)
+
+# events list
+new_user.events.list()
+
foobar_user = gl.users.create(
{'email': 'foobar@example.com', 'username': 'foobar',
'name': 'Foo Bar', 'password': 'foobar_password'})
@@ -135,6 +135,7 @@ assert(len(new_user.emails.list()) == 0)
attrs = new_user.customattributes.list()
assert(len(attrs) == 0)
attr = new_user.customattributes.set('key', 'value1')
+assert(len(gl.users.list(custom_attributes={'key': 'value1'})) == 1)
assert(attr.key == 'key')
assert(attr.value == 'value1')
assert(len(new_user.customattributes.list()) == 1)
@@ -210,8 +211,9 @@ p_id = gl.groups.list(search='group2')[0].id
group3 = gl.groups.create({'name': 'group3', 'path': 'group3', 'parent_id': p_id})
assert(len(gl.groups.list()) == 3)
-assert(len(gl.groups.list(search='1')) == 1)
+assert(len(gl.groups.list(search='oup1')) == 1)
assert(group3.parent_id == p_id)
+assert(group2.subgroups.list()[0].id == group3.id)
group1.members.create({'access_level': gitlab.Group.OWNER_ACCESS,
'user_id': user1.id})
@@ -235,6 +237,21 @@ assert(member.access_level == gitlab.Group.OWNER_ACCESS)
group2.members.delete(gl.user.id)
+# group custom attributes
+attrs = group2.customattributes.list()
+assert(len(attrs) == 0)
+attr = group2.customattributes.set('key', 'value1')
+assert(len(gl.groups.list(custom_attributes={'key': 'value1'})) == 1)
+assert(attr.key == 'key')
+assert(attr.value == 'value1')
+assert(len(group2.customattributes.list()) == 1)
+attr = group2.customattributes.set('key', 'value2')
+attr = group2.customattributes.get('key')
+assert(attr.value == 'value2')
+assert(len(group2.customattributes.list()) == 1)
+attr.delete()
+assert(len(group2.customattributes.list()) == 0)
+
# group notification settings
settings = group2.notificationsettings.get()
settings.level = 'disabled'
@@ -290,6 +307,30 @@ assert(len(l1) == 1)
assert(len(l2) == 1)
assert(l1[0].id != l2[0].id)
+# group custom attributes
+attrs = admin_project.customattributes.list()
+assert(len(attrs) == 0)
+attr = admin_project.customattributes.set('key', 'value1')
+assert(len(gl.projects.list(custom_attributes={'key': 'value1'})) == 1)
+assert(attr.key == 'key')
+assert(attr.value == 'value1')
+assert(len(admin_project.customattributes.list()) == 1)
+attr = admin_project.customattributes.set('key', 'value2')
+attr = admin_project.customattributes.get('key')
+assert(attr.value == 'value2')
+assert(len(admin_project.customattributes.list()) == 1)
+attr.delete()
+assert(len(admin_project.customattributes.list()) == 0)
+
+# project pages domains
+domain = admin_project.pagesdomains.create({'domain': 'foo.domain.com'})
+assert(len(admin_project.pagesdomains.list()) == 1)
+assert(len(gl.pagesdomains.list()) == 1)
+domain = admin_project.pagesdomains.get('foo.domain.com')
+assert(domain.domain == 'foo.domain.com')
+domain.delete()
+assert(len(admin_project.pagesdomains.list()) == 0)
+
# project content (files)
admin_project.files.create({'file_path': 'README',
'branch': 'master',
@@ -370,7 +411,7 @@ assert(env.external_url == 'http://new.env/whatever')
env.delete()
assert(len(admin_project.environments.list()) == 0)
-# events
+# project events
admin_project.events.list()
# forks
@@ -436,6 +477,10 @@ assert(len(admin_project.issues.list(milestone='milestone1')) == 1)
assert(m1.issues().next().title == 'my issue 1')
note = issue1.notes.create({'body': 'This is an issue note'})
assert(len(issue1.notes.list()) == 1)
+emoji = note.awardemojis.create({'name': 'tractor'})
+assert(len(note.awardemojis.list()) == 1)
+emoji.delete()
+assert(len(note.awardemojis.list()) == 0)
note.delete()
assert(len(issue1.notes.list()) == 0)
assert(isinstance(issue1.user_agent_detail(), dict))
@@ -488,6 +533,12 @@ admin_project.files.create({'file_path': 'README2.rst',
mr = admin_project.mergerequests.create({'source_branch': 'branch1',
'target_branch': 'master',
'title': 'MR readme2'})
+
+# basic testing: only make sure that the methods exist
+mr.commits()
+mr.changes()
+#mr.participants() # not yet available
+
mr.merge()
admin_project.branches.delete('branch1')
@@ -543,6 +594,11 @@ assert(len(ns) != 0)
ns = gl.namespaces.list(search='root', all=True)[0]
assert(ns.kind == 'user')
+# features
+feat = gl.features.set('foo', 30)
+assert(feat.name == 'foo')
+assert(len(gl.features.list()) == 1)
+
# broadcast messages
msg = gl.broadcastmessages.create({'message': 'this is the message'})
msg.color = '#444444'
@@ -587,3 +643,6 @@ snippet.delete()
# user activities
gl.user_activities.list()
+
+# events
+gl.events.list()