pax_global_header00006660000000000000000000000064132422415020014505gustar00rootroot0000000000000052 comment=10bd1f43f59b2257e6195b290b0dc8a578b7562a python-gitlab-1.3.0/000077500000000000000000000000001324224150200142675ustar00rootroot00000000000000python-gitlab-1.3.0/.gitignore000066400000000000000000000001271324224150200162570ustar00rootroot00000000000000*.pyc build/ dist/ MANIFEST .*.swp *.egg-info .idea/ docs/_build .testrepository/ .tox python-gitlab-1.3.0/.testr.conf000066400000000000000000000002441324224150200163550ustar00rootroot00000000000000[DEFAULT] test_command=${PYTHON:-python} -m subunit.run discover -t ./ ./gitlab/tests $LISTOPT $IDOPTION test_id_option=--load-list $IDFILE test_list_option=--list python-gitlab-1.3.0/.travis.yml000066400000000000000000000006011324224150200163750ustar00rootroot00000000000000sudo: required services: - docker addons: apt: sources: - deadsnakes packages: - python3.5 language: python python: 2.7 env: - TOX_ENV=py35 - TOX_ENV=py34 - TOX_ENV=py27 - TOX_ENV=pep8 - TOX_ENV=docs - TOX_ENV=py_func_v3 - TOX_ENV=py_func_v4 - TOX_ENV=cli_func_v3 - TOX_ENV=cli_func_v4 install: - pip install tox script: - tox -e $TOX_ENV python-gitlab-1.3.0/AUTHORS000066400000000000000000000065051324224150200153450ustar00rootroot00000000000000Authors ------- Gauvain Pocentek Mika Mäenpää Contributors ------------ Adam Reid Alexander Skiba Alex Widener Amar Sood (tekacs) Andjelko Horvat Andreas Nüßlein Andrew Austin Armin Weihbold Aron Pammer Asher256 Bancarel Valentin Ben Brown Carlo Mion Carlos Soriano Christian Christian Wenk Colin D Bennett Cosimo Lupo Crestez Dan Leonard Daniel Kimsey derek-austin Diego Giovane Pasqualin Dmytro Litvinov Eli Sarver Eric L Frederich Erik Weatherwax fgouteroux Greg Allen Guillaume Delacour Guyzmo hakkeroid Ian Sparks itxaka Ivica Arsov James (d0c_s4vage) Johnson James E. Flemer James Johnson Jamie Bliss Jason Antman Jerome Robert Johan Brandhorst Jonathon Reinhart Jon Banafato Keith Wansbrough Koen Smets Kris Gambirazzi Lyudmil Nenov Mart Sõmermaa massimone88 Matej Zerovnik Matt Odden Maura Hausman Michael Overmeyer Michal Galet Mike Kobit Mikhail Lopotkov Miouge1 Missionrulz Mond WAN Moritz Lipp Nathan Giesbrecht Nathan Schmidt pa4373 Patrick Miller Pavel Savchenko Peng Xiao Pete Browne Peter Mosmans P. F. Chimento Philipp Busch Pierre Tardy Rafael Eyng Richard Hansen Robert Lu samcday savenger Stefan K. Dunkler Stefan Klug Stefano Mandruzzato THEBAULT Julien Tim Neumann Will Starms Yosi Zelensky python-gitlab-1.3.0/COPYING000066400000000000000000000167431324224150200153350ustar00rootroot00000000000000 GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. python-gitlab-1.3.0/ChangeLog.rst000066400000000000000000000525751324224150200166660ustar00rootroot00000000000000ChangeLog ========= Version 1.3.0_ - 2018-02-18 --------------------------- * Add support for pipeline schedules and schedule variables * Clarify information about supported python version * Add manager for jobs within a pipeline * Fix wrong tag example * Update the groups documentation * Add support for MR participants API * Add support for getting list of user projects * Add Gitlab and User events support * Make trigger_pipeline return the pipeline * Config: support api_version in the global section * Gitlab can be used as context manager * Default to API v4 * Add a simplified example for streamed artifacts * Add documentation about labels update 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 --------------------------- * Fix trigger variables in v4 API * Make the delete() method handle / in ids * [docs] update the file upload samples * Tags release description: support / in tag names * [docs] improve the labels usage documentation * Add support for listing project users * ProjectFileManager.create: handle / in file paths * Change ProjectUser and GroupProject base class * [docs] document `get_create_attrs` in the API tutorial * Document the Gitlab session parameter * ProjectFileManager: custom update() method * Project: add support for printing_merge_request_link_enabled attr * Update the ssl_verify docstring * Add support for group milestones * Add support for GPG keys * Add support for wiki pages * Update the repository_blob documentation * Fix the CLI for objects without ID (API v4) * Add a contributed Dockerfile * Pagination generators: expose more information * Module's base objects serialization * [doc] Add sample code for client-side certificates Version 1.0.2_ - 2017-09-29 --------------------------- * [docs] remove example usage of submanagers * Properly handle the labels attribute in ProjectMergeRequest * ProjectFile: handle / in path for delete() and save() Version 1.0.1_ - 2017-09-21 --------------------------- * Tags can be retrieved by ID * Add the server response in GitlabError exceptions * Add support for project file upload * Minor typo fix in "Switching to v4" documentation * Fix password authentication for v4 * Fix the labels attrs on MR and issues * Exceptions: use a proper error message * Fix http_get method in get artifacts and job trace * CommitStatus: `sha` is parent attribute * Fix a couple listing calls to allow proper pagination * Add missing doc file Version 1.0.0_ - 2017-09-08 --------------------------- * Support for API v4. See http://python-gitlab.readthedocs.io/en/master/switching-to-v4.html * Support SSL verification via internal CA bundle * Docs: Add link to gitlab docs on obtaining a token * Added dependency injection support for Session * Fixed repository_compare examples * Fix changelog and release notes inclusion in sdist * Missing expires_at in GroupMembers update * Add lower-level methods for Gitlab() Version 0.21.2_ - 2017-06-11 ---------------------------- * Install doc: use sudo for system commands * [v4] Make MR work properly * Remove extra_attrs argument from _raw_list * [v4] Make project issues work properly * Fix urlencode() usage (python 2/3) (#268) * Fixed spelling mistake (#269) * Add new event types to ProjectHook Version 0.21.1_ - 2017-05-25 ---------------------------- * Fix the manager name for jobs in the Project class * Fix the docs Version 0.21_ - 2017-05-24 -------------------------- * Add time_stats to ProjectMergeRequest * Update User options for creation and update (#246) * Add milestone.merge_requests() API * Fix docs typo (s/correspnding/corresponding/) * Support milestone start date (#251) * Add support for priority attribute in labels (#256) * Add support for nested groups (#257) * Make GroupProjectManager a subclass of ProjectManager (#255) * Available services: return a list instead of JSON (#258) * MR: add support for time tracking features (#248) * Fixed repository_tree and repository_blob path encoding (#265) * Add 'search' attribute to projects.list() * Initial gitlab API v4 support * Reorganise the code to handle v3 and v4 objects * Allow 202 as delete return code * Deprecate parameter related methods in gitlab.Gitlab Version 0.20_ - 2017-03-25 --------------------------- * Add time tracking support (#222) * Improve changelog (#229, #230) * Make sure that manager objects are never overwritten (#209) * Include chanlog and release notes in docs * Add DeployKey{,Manager} classes (#212) * Add support for merge request notes deletion (#227) * Properly handle extra args when listing with all=True (#233) * Implement pipeline creation API (#237) * Fix spent_time methods * Add 'delete source branch' option when creating MR (#241) * Provide API wrapper for cherry picking commits (#236) * Stop listing if recursion limit is hit (#234) Version 0.19_ - 2017-02-21 --------------------------- * 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_ - 2016-12-27 --------------------------- * Fix JIRA service editing for GitLab 8.14+ * Add jira_issue_transition_id to the JIRA service optional fields * Added support for Snippets (new API in Gitlab 8.15) * [docs] update pagination section * [docs] artifacts example: open file in wb mode * [CLI] ignore empty arguments * [CLI] Fix wrong use of arguments * [docs] Add doc for snippets * Fix duplicated data in API docs * Update known attributes for projects * sudo: always use strings Version 0.17_ - 2016-12-02 --------------------------- * README: add badges for pypi and RTD * Fix ProjectBuild.play (raised error on success) * Pass kwargs to the object factory * Add .tox to ignore to respect default tox settings * Convert response list to single data source for iid requests * Add support for boards API * Add support for Gitlab.version() * Add support for broadcast messages API * Add support for the notification settings API * Don't overwrite attributes returned by the server * Fix bug when retrieving changes for merge request * Feature: enable / disable the deploy key in a project * Docs: add a note for python 3.5 for file content update * ProjectHook: support the token attribute * Rework the API documentation * Fix docstring for http_{username,password} * Build managers on demand on GitlabObject's * API docs: add managers doc in GitlabObject's * Sphinx ext: factorize the build methods * Implement __repr__ for gitlab objects * Add a 'report a bug' link on doc * Remove deprecated methods * Implement merge requests diff support * Make the manager objects creation more dynamic * Add support for templates API * Add attr 'created_at' to ProjectIssueNote * Add attr 'updated_at' to ProjectIssue * CLI: add support for project all --all * Add support for triggering a new build * Rework requests arguments (support latest requests release) * Fix `should_remove_source_branch` Version 0.16_ - 2016-10-16 --------------------------- * Add the ability to fork to a specific namespace * JIRA service - add api_url to optional attributes * Fix bug: Missing coma concatenates array values * docs: branch protection notes * Create a project in a group * Add only_allow_merge_if_build_succeeds option to project objects * Add support for --all in CLI * Fix examples for file modification * Use the plural merge_requests URL everywhere * Rework travis and tox setup * Workaround gitlab setup failure in tests * Add ProjectBuild.erase() * Implement ProjectBuild.play() Version 0.15.1_ - 2016-10-16 ----------------------------- * docs: improve the pagination section * Fix and test pagination * 'path' is an existing gitlab attr, don't use it as method argument Version 0.15_ - 2016-08-28 --------------------------- * Add a basic HTTP debug method * Run more tests in travis * Fix fork creation documentation * Add more API examples in docs * Update the ApplicationSettings attributes * Implement the todo API * Add sidekiq metrics support * Move the constants at the gitlab root level * Remove methods marked as deprecated 7 months ago * Refactor the Gitlab class * Remove _get_list_or_object() and its tests * Fix canGet attribute (typo) * Remove unused ProjectTagReleaseManager class * Add support for project services API * Add support for project pipelines * Add support for access requests * Add support for project deployments Version 0.14_ - 2016-08-07 --------------------------- * Remove 'next_url' from kwargs before passing it to the cls constructor. * List projects under group * Add support for subscribe and unsubscribe in issues * Project issue: doc and CLI for (un)subscribe * Added support for HTTP basic authentication * Add support for build artifacts and trace * --title is a required argument for ProjectMilestone * Commit status: add optional context url * Commit status: optional get attrs * Add support for commit comments * Issues: add optional listing parameters * Issues: add missing optional listing parameters * Project issue: proper update attributes * Add support for project-issue move * Update ProjectLabel attributes * Milestone: optional listing attrs * Add support for namespaces * Add support for label (un)subscribe * MR: add (un)subscribe support * Add `note_events` to project hooks attributes * Add code examples for a bunch of resources * Implement user emails support * Project: add VISIBILITY_* constants * Fix the Project.archive call * Implement archive/unarchive for a projet * Update ProjectSnippet attributes * Fix ProjectMember update * Implement sharing project with a group * Implement CLI for project archive/unarchive/share * Implement runners global API * Gitlab: add managers for build-related resources * Implement ProjectBuild.keep_artifacts * Allow to stream the downloads when appropriate * Groups can be updated * Replace Snippet.Content() with a new content() method * CLI: refactor _die() * Improve commit statuses and comments * Add support from listing group issues * Added a new project attribute to enable the container registry. * Add a contributing section in README * Add support for global deploy key listing * Add support for project environments * MR: get list of changes and commits * Fix the listing of some resources * MR: fix updates * Handle empty messages from server in exceptions * MR (un)subscribe: don't fail if state doesn't change * MR merge(): update the object Version 0.13_ - 2016-05-16 --------------------------- * Add support for MergeRequest validation * MR: add support for cancel_merge_when_build_succeeds * MR: add support for closes_issues * Add "external" parameter for users * Add deletion support for issues and MR * Add missing group creation parameters * Add a Session instance for all HTTP requests * Enable updates on ProjectIssueNotes * Add support for Project raw_blob * Implement project compare * Implement project contributors * Drop the next_url attribute when listing * Remove unnecessary canUpdate property from ProjectIssuesNote * Add new optional attributes for projects * Enable deprecation warnings for gitlab only * Rework merge requests update * Rework the Gitlab.delete method * ProjectFile: file_path is required for deletion * Rename some methods to better match the API URLs * Deprecate the file_* methods in favor of the files manager * Implement star/unstar for projects * Implement list/get licenses * Manage optional parameters for list() and get() Version 0.12.2_ - 2016-03-19 ----------------------------- * Add new `ProjectHook` attributes * Add support for user block/unblock * Fix GitlabObject creation in _custom_list * Add support for more CLI subcommands * Add some unit tests for CLI * Add a coverage tox env * Define GitlabObject.as_dict() to dump object as a dict * Define GitlabObject.__eq__() and __ne__() equivalence methods * Define UserManager.search() to search for users * Define UserManager.get_by_username() to get a user by username * Implement "user search" CLI * Improve the doc for UserManager * CLI: implement user get-by-username * Re-implement _custom_list in the Gitlab class * Fix the 'invalid syntax' error on Python 3.2 * Gitlab.update(): use the proper attributes if defined Version 0.12.1_ - 2016-02-03 ----------------------------- * Fix a broken upload to pypi Version 0.12_ - 2016-02-03 --------------------------- * Improve documentation * Improve unit tests * Improve test scripts * Skip BaseManager attributes when encoding to JSON * Fix the json() method for python 3 * Add Travis CI support * Add a decode method for ProjectFile * Make connection exceptions more explicit * Fix ProjectLabel get and delete * Implement ProjectMilestone.issues() * ProjectTag supports deletion * Implement setting release info on a tag * Implement project triggers support * Implement project variables support * Add support for application settings * Fix the 'password' requirement for User creation * Add sudo support * Fix project update * Fix Project.tree() * Add support for project builds Version 0.11.1_ - 2016-01-17 ----------------------------- * Fix discovery of parents object attrs for managers * Support setting commit status * Support deletion without getting the object first * Improve the documentation Version 0.11_ - 2016-01-09 --------------------------- * functional_tests.sh: support python 2 and 3 * Add a get method for GitlabObject * CLI: Add the -g short option for --gitlab * Provide a create method for GitlabObject's * Rename the _created attribute _from_api * More unit tests * CLI: fix error when arguments are missing (python 3) * Remove deprecated methods * Implement managers to get access to resources * Documentation improvements * Add fork project support * Deprecate the "old" Gitlab methods * Add support for groups search Version 0.10_ - 2015-12-29 --------------------------- * Implement pagination for list() (#63) * Fix url when fetching a single MergeRequest * Add support to update MergeRequestNotes * API: Provide a Gitlab.from_config method * setup.py: require requests>=1 (#69) * Fix deletion of object not using 'id' as ID (#68) * Fix GET/POST for project files * Make 'confirm' an optional attribute for user creation * Python 3 compatibility fixes * Add support for group members update (#73) Version 0.9.2_ - 2015-07-11 ---------------------------- * CLI: fix the update and delete subcommands (#62) Version 0.9.1_ - 2015-05-15 ---------------------------- * Fix the setup.py script Version 0.9_ - 2015-05-15 -------------------------- * Implement argparse libray for parsing argument on CLI * Provide unit tests and (a few) functional tests * Provide PEP8 tests * Use tox to run the tests * CLI: provide a --config-file option * Turn the gitlab module into a proper package * Allow projects to be updated * Use more pythonic names for some methods * Deprecate some Gitlab object methods: - raw* methods should never have been exposed; replace them with _raw_* methods - setCredentials and setToken are replaced with set_credentials and set_token * Sphinx: don't hardcode the version in conf.py Version 0.8_ - 2014-10-26 -------------------------- * Better python 2.6 and python 3 support * Timeout support in HTTP requests * Gitlab.get() raised GitlabListError instead of GitlabGetError * Support api-objects which don't have id in api response * Add ProjectLabel and ProjectFile classes * Moved url attributes to separate list * Added list for delete attributes Version 0.7_ - 2014-08-21 -------------------------- * Fix license classifier in setup.py * Fix encoding error when printing to redirected output * Fix encoding error when updating with redirected output * Add support for UserKey listing and deletion * Add support for branches creation and deletion * Support state_event in ProjectMilestone (#30) * Support namespace/name for project id (#28) * Fix handling of boolean values (#22) Version 0.6_ - 2014-01-16 -------------------------- * IDs can be unicode (#15) * ProjectMember: constructor should not create a User object * Add support for extra parameters when listing all projects (#12) * Projects listing: explicitly define arguments for pagination Version 0.5_ - 2013-12-26 -------------------------- * Add SSH key for user * Fix comments * Add support for project events * Support creation of projects for users * Project: add methods for create/update/delete files * Support projects listing: search, all, owned * System hooks can't be updated * Project.archive(): download tarball of the project * Define new optional attributes for user creation * Provide constants for access permissions in groups Version 0.4_ - 2013-09-26 -------------------------- * Fix strings encoding (Closes #6) * Allow to get a project commit (GitLab 6.1) * ProjectMergeRequest: fix Note() method * Gitlab 6.1 methods: diff, blob (commit), tree, blob (project) * Add support for Gitlab 6.1 group members Version 0.3_ - 2013-08-27 -------------------------- * Use PRIVATE-TOKEN header for passing the auth token * provide a AUTHORS file * cli: support ssl_verify config option * Add ssl_verify option to Gitlab object. Defauls to True * Correct url for merge requests API. Version 0.2_ - 2013-08-08 -------------------------- * provide a pip requirements.txt * drop some debug statements Version 0.1 - 2013-07-08 ------------------------ * Initial release .. _1.3.0: https://github.com/python-gitlab/python-gitlab/compare/1.2.0...1.3.0 .. _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 .. _1.0.0: https://github.com/python-gitlab/python-gitlab/compare/0.21.2...1.0.0 .. _0.21.2: https://github.com/python-gitlab/python-gitlab/compare/0.21.1...0.21.2 .. _0.21.1: https://github.com/python-gitlab/python-gitlab/compare/0.21...0.21.1 .. _0.21: https://github.com/python-gitlab/python-gitlab/compare/0.20...0.21 .. _0.20: https://github.com/python-gitlab/python-gitlab/compare/0.19...0.20 .. _0.19: https://github.com/python-gitlab/python-gitlab/compare/0.18...0.19 .. _0.18: https://github.com/python-gitlab/python-gitlab/compare/0.17...0.18 .. _0.17: https://github.com/python-gitlab/python-gitlab/compare/0.16...0.17 .. _0.16: https://github.com/python-gitlab/python-gitlab/compare/0.15.1...0.16 .. _0.15.1: https://github.com/python-gitlab/python-gitlab/compare/0.15...0.15.1 .. _0.15: https://github.com/python-gitlab/python-gitlab/compare/0.14...0.15 .. _0.14: https://github.com/python-gitlab/python-gitlab/compare/0.13...0.14 .. _0.13: https://github.com/python-gitlab/python-gitlab/compare/0.12.2...0.13 .. _0.12.2: https://github.com/python-gitlab/python-gitlab/compare/0.12.1...0.12.2 .. _0.12.1: https://github.com/python-gitlab/python-gitlab/compare/0.12...0.12.1 .. _0.12: https://github.com/python-gitlab/python-gitlab/compare/0.11.1...0.12 .. _0.11.1: https://github.com/python-gitlab/python-gitlab/compare/0.11...0.11.1 .. _0.11: https://github.com/python-gitlab/python-gitlab/compare/0.10...0.11 .. _0.10: https://github.com/python-gitlab/python-gitlab/compare/0.9.2...0.10 .. _0.9.2: https://github.com/python-gitlab/python-gitlab/compare/0.9.1...0.9.2 .. _0.9.1: https://github.com/python-gitlab/python-gitlab/compare/0.9...0.9.1 .. _0.9: https://github.com/python-gitlab/python-gitlab/compare/0.8...0.9 .. _0.8: https://github.com/python-gitlab/python-gitlab/compare/0.7...0.8 .. _0.7: https://github.com/python-gitlab/python-gitlab/compare/0.6...0.7 .. _0.6: https://github.com/python-gitlab/python-gitlab/compare/0.5...0.6 .. _0.5: https://github.com/python-gitlab/python-gitlab/compare/0.4...0.5 .. _0.4: https://github.com/python-gitlab/python-gitlab/compare/0.3...0.4 .. _0.3: https://github.com/python-gitlab/python-gitlab/compare/0.2...0.3 .. _0.2: https://github.com/python-gitlab/python-gitlab/compare/0.1...0.2 python-gitlab-1.3.0/MANIFEST.in000066400000000000000000000003701324224150200160250ustar00rootroot00000000000000include COPYING AUTHORS ChangeLog.rst RELEASE_NOTES.rst requirements.txt test-requirements.txt rtd-requirements.txt include tox.ini .testr.conf .travis.yml recursive-include tools * recursive-include docs *j2 *.py *.rst api/*.rst Makefile make.bat python-gitlab-1.3.0/README.rst000066400000000000000000000055471324224150200157710ustar00rootroot00000000000000.. image:: https://travis-ci.org/python-gitlab/python-gitlab.svg?branch=master :target: https://travis-ci.org/python-gitlab/python-gitlab .. image:: https://badge.fury.io/py/python-gitlab.svg :target: https://badge.fury.io/py/python-gitlab .. 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 ============= ``python-gitlab`` is a Python package providing access to the GitLab server API. It supports the v3 and v4 APIs of GitLab, and provides a CLI tool (``gitlab``). Installation ============ Requirements ------------ python-gitlab depends on: * `python-requests `_ * `six `_ Install with pip ---------------- .. code-block:: console pip install python-gitlab Bug reports =========== Please report bugs and feature requests at https://github.com/python-gitlab/python-gitlab/issues. Documentation ============= The full documentation for CLI and API is available on `readthedocs `_. Contributing ============ You can contribute to the project in multiple ways: * Write documentation * Implement features * Fix bugs * Add unit and functional tests * Everything else you can think of Provide your patches as github pull requests. Thanks! Running unit tests ------------------ Before submitting a pull request make sure that the tests still succeed with your change. Unit tests will run using the travis service and passing tests are mandatory. You need to install ``tox`` to run unit tests and documentation builds: .. code-block:: bash # run the unit tests for python 2/3, and the pep8 tests: tox # run tests in one environment only: tox -epy35 # build the documentation, the result will be generated in # build/sphinx/html/ tox -edocs Running integration tests ------------------------- Two scripts run tests against a running gitlab instance, using a docker container. You need to have docker installed on the test machine, and your user must have the correct permissions to talk to the docker daemon. To run these tests: .. code-block:: bash # run the CLI tests: ./tools/functional_tests.sh # run the python API tests: ./tools/py_functional_tests.sh You can also build a test environment using the following command: .. code-block:: bash ./tools/build_test_env.sh A freshly configured gitlab container will be available at http://localhost:8080 (login ``root`` / password ``5iveL!fe``). A configuration for python-gitlab will be written in ``/tmp/python-gitlab.cfg``. To cleanup the environment delete the container: .. code-block:: bash docker rm -f gitlab-test python-gitlab-1.3.0/RELEASE_NOTES.rst000066400000000000000000000102531324224150200170520ustar00rootroot00000000000000############# 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 ``DeployKeyManager`` instead * gitlab.v3.objects.Project ``archive_`` and ``unarchive_`` methods * gitlab.Gitlab ``credentials_auth``, ``token_auth``, ``set_url``, ``set_token`` and ``set_credentials`` methods. Once a Gitlab object has been created its URL and authentication information cannot be updated: create a new Gitlab object if you need to use new information * The ``todo()`` method raises a ``GitlabTodoError`` exception on error Changes from 1.0.2 to 1.1 ========================= * The ``ProjectUser`` class doesn't inherit from ``User`` anymore, and the ``GroupProject`` class doesn't inherit from ``Project`` anymore. The Gitlab API doesn't provide the same set of features for these objects, so python-gitlab objects shouldn't try to workaround that. You can create ``User`` or ``Project`` objects from ``ProjectUser`` and ``GroupProject`` objects using the ``id`` attribute: .. code-block:: python for gr_project in group.projects.list(): # lazy object creation avoids a Gitlab API request project = gl.projects.get(gr_project.id, lazy=True) project.default_branch = 'develop' project.save() Changes from 0.21 to 1.0.0 ========================== 1.0.0 brings a stable python-gitlab API for the v4 Gitlab API. v3 is still used by default. v4 is mostly compatible with the v3, but some important changes have been introduced. Make sure to read `Switching to GtiLab API v4 `_. The development focus will be v4 from now on. v3 has been deprecated by GitLab and will disappear from python-gitlab at some point. Changes from 0.20 to 0.21 ========================= * Initial support for the v4 API (experimental) The support for v4 is stable enough to be tested, but some features might be broken. Please report issues to https://github.com/python-gitlab/python-gitlab/issues/ Be aware that the python-gitlab API for v4 objects might change in the next releases. .. warning:: Consider defining explicitly which API version you want to use in the configuration files or in your ``gitlab.Gitlab`` instances. The default will change from v3 to v4 soon. * Several methods have been deprecated in the ``gitlab.Gitlab`` class: + ``credentials_auth()`` is deprecated and will be removed. Call ``auth()``. + ``token_auth()`` is deprecated and will be removed. Call ``auth()``. + ``set_url()`` is deprecated, create a new ``Gitlab`` instance if you need an updated URL. + ``set_token()`` is deprecated, use the ``private_token`` argument of the ``Gitlab`` constructor. + ``set_credentials()`` is deprecated, use the ``email`` and ``password`` arguments of the ``Gitlab`` constructor. * The service listing method (``ProjectServiceManager.list()``) now returns a python list instead of a JSON string. Changes from 0.19 to 0.20 ========================= * The ``projects`` attribute of ``Group`` objects is not a list of ``Project`` objects anymore. It is a Manager object giving access to ``GroupProject`` objects. To get the list of projects use: .. code-block:: python group.projects.list() Documentation: http://python-gitlab.readthedocs.io/en/stable/gl_objects/groups.html#examples Related issue: https://github.com/python-gitlab/python-gitlab/issues/209 * The ``Key`` objects are deprecated in favor of the new ``DeployKey`` objects. They are exactly the same but the name makes more sense. Documentation: http://python-gitlab.readthedocs.io/en/stable/gl_objects/deploy_keys.html Related issue: https://github.com/python-gitlab/python-gitlab/issues/212 python-gitlab-1.3.0/contrib/000077500000000000000000000000001324224150200157275ustar00rootroot00000000000000python-gitlab-1.3.0/contrib/docker/000077500000000000000000000000001324224150200171765ustar00rootroot00000000000000python-gitlab-1.3.0/contrib/docker/Dockerfile000066400000000000000000000004041324224150200211660ustar00rootroot00000000000000FROM python:slim # Install python-gitlab RUN pip install --upgrade python-gitlab # Copy sample configuration file COPY python-gitlab.cfg / # Define the entrypoint that enable a configuration file ENTRYPOINT ["gitlab", "--config-file", "/python-gitlab.cfg"] python-gitlab-1.3.0/contrib/docker/README.rst000066400000000000000000000007631324224150200206730ustar00rootroot00000000000000python-gitlab docker image ========================== Dockerfile contributed by *oupala*: https://github.com/python-gitlab/python-gitlab/issues/295 How to build ------------ ``docker build -t me/python-gitlab:VERSION .`` How to use ---------- ``docker run -it -v /path/to/python-gitlab.cfg:/python-gitlab.cfg python-gitlab ...`` To make things easier you can create a shell alias: ``alias gitlab='docker run --rm -it -v /path/to/python-gitlab.cfg:/python-gitlab.cfg python-gitlab`` python-gitlab-1.3.0/contrib/docker/python-gitlab.cfg000066400000000000000000000004031324224150200224350ustar00rootroot00000000000000[global] default = somewhere ssl_verify = true timeout = 5 api_version = 3 [somewhere] url = https://some.whe.re private_token = vTbFeqJYCY3sibBP7BZM api_version = 4 [elsewhere] url = http://else.whe.re:8080 private_token = CkqsjqcQSFH5FQKDccu4 timeout = 1 python-gitlab-1.3.0/docs/000077500000000000000000000000001324224150200152175ustar00rootroot00000000000000python-gitlab-1.3.0/docs/Makefile000066400000000000000000000152061324224150200166630ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-gitlab.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-gitlab.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/python-gitlab" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/python-gitlab" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." python-gitlab-1.3.0/docs/_templates/000077500000000000000000000000001324224150200173545ustar00rootroot00000000000000python-gitlab-1.3.0/docs/_templates/breadcrumbs.html000066400000000000000000000017471324224150200225440ustar00rootroot00000000000000{# Support for Sphinx 1.3+ page_source_suffix, but don't break old builds. #} {% if page_source_suffix %} {% set suffix = page_source_suffix %} {% else %} {% set suffix = source_suffix %} {% endif %}

python-gitlab-1.3.0/docs/api-objects.rst000066400000000000000000000014101324224150200201450ustar00rootroot00000000000000############ API examples ############ .. toctree:: :maxdepth: 1 gl_objects/access_requests gl_objects/emojis gl_objects/branches gl_objects/protected_branches gl_objects/messages gl_objects/builds gl_objects/commits 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 gl_objects/notifications gl_objects/mrs gl_objects/namespaces gl_objects/milestones gl_objects/pagesdomains gl_objects/projects gl_objects/runners gl_objects/settings gl_objects/snippets gl_objects/system_hooks gl_objects/templates gl_objects/todos gl_objects/users gl_objects/sidekiq gl_objects/wikis python-gitlab-1.3.0/docs/api-usage.rst000066400000000000000000000225071324224150200176320ustar00rootroot00000000000000############################ Getting started with the API ############################ 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. v4 is the default API used by python-gitlab since version 1.3.0. ``gitlab.Gitlab`` class ======================= To connect to a GitLab server, create a ``gitlab.Gitlab`` object: .. code-block:: python import gitlab # 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 (for GitLab << 10.2) gl = gitlab.Gitlab('http://10.0.0.1', email='jdoe', password='s3cr3t') # anonymous gitlab instance, read-only for public resources gl = gitlab.Gitlab('http://10.0.0.1') # make an API request to create the gl.user object. This is mandatory if you # use the username/password authentication. gl.auth() You can also use configuration files to create ``gitlab.Gitlab`` objects: .. code-block:: python gl = gitlab.Gitlab.from_config('somewhere', ['/tmp/gl.cfg']) 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 `_ for a detailed discussion. API version =========== ``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=3) .. warning:: The python-gitlab API is not the same for v3 and v4. Make sure to read :ref:`switching_to_v4` if you are upgrading from v3. Managers ======== The ``gitlab.Gitlab`` class provides managers to access the GitLab resources. Each manager provides a set of methods to act on the resources. The available methods depend on the resource type. Examples: .. code-block:: python # list all the projects projects = gl.projects.list() for project in projects: print(project) # get the group with id == 2 group = gl.groups.get(2) for group in groups: print() # create a new user user_data = {'email': 'jen@foo.com', 'username': 'jen', 'name': 'Jen'} user = gl.users.create(user_data) print(user) You can list the mandatory and optional attributes for object creation with the manager's ``get_create_attrs()`` method. It returns 2 tuples, the first one is the list of mandatory attributes, the second one the list of optional attribute: .. code-block:: python # v4 only print(gl.projects.get_create_attrs()) (('name',), ('path', 'namespace_id', ...)) 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 for v3, or the ``attributes`` attribute for v4: .. code-block:: python project = gl.projects.get(1) # v3 print(vars(project)) # or print(project.__dict__) # v4 print(project.attributes) Some objects also provide managers to access related GitLab resources: .. code-block:: python # list the issues for a project project = gl.projects.get(1) issues = project.issues.list() Gitlab Objects ============== You can update or delete a remote object when it exists locally: .. code-block:: python # update the attributes of a resource project = gl.projects.get(1) project.wall_enabled = False # don't forget to apply your changes on the server: project.save() # delete the resource project.delete() Some classes provide additional methods, allowing more actions on the GitLab resources. For example: .. code-block:: python # star a git repository project = gl.projects.get(1) project.star() Base types ========== The ``gitlab`` package provides some base types. * ``gitlab.Gitlab`` is the primary class, handling the HTTP requests. It holds the GitLab URL and authentication information. For v4 the following types are defined: * ``gitlab.base.RESTObject`` is the base class for all the GitLab v4 objects. These objects provide an abstraction for GitLab resources (projects, groups, and so on). * ``gitlab.base.RESTManager`` is the base class for v4 objects managers, providing the API to manipulate the resources and their attributes. For v3 the following types are defined: * ``gitlab.base.GitlabObject`` is the base class for all the GitLab v3 objects. These objects provide an abstraction for GitLab resources (projects, groups, and so on). * ``gitlab.base.BaseManager`` is the base class for v3 objects managers, providing the API to manipulate the resources and their attributes. Lazy objects (v4 only) ====================== To avoid useless calls to the server API, you can create lazy objects. These objects are created locally using a known ID, and give access to other managers and methods. The following exemple will only make one API call to the GitLab server to star a project: .. code-block:: python # star a git repository project = gl.projects.get(1, lazy=True) # no API call project.star() # API call Pagination ========== You can use pagination to iterate over long lists. All the Gitlab objects listing methods support the ``page`` and ``per_page`` parameters: .. code-block:: python ten_first_groups = gl.groups.list(page=1, per_page=10) .. note:: 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: .. code-block:: python all_groups = gl.groups.list(all=True) all_owned_projects = gl.projects.owned(all=True) .. warning:: python-gitlab will iterate over the list by calling the corresponding API multiple times. This might take some time if you have a lot of items to retrieve. This might also consume a lot of memory as all the items will be stored in RAM. If you're encountering the python recursion limit exception, use ``safe_all=True`` instead to stop pagination automatically if the recursion limit is hit. With v4, ``list()`` methods can also return a generator object which will handle the next calls to the API when required: .. code-block:: python items = gl.groups.list(as_list=False) for item in items: print(item.attributes) The generator exposes extra listing information as received by the server: * ``current_page``: current page number (first page is 1) * ``prev_page``: if ``None`` the current page is the first one * ``next_page``: if ``None`` the current page is the last one * ``per_page``: number of items per page * ``total_pages``: total number of pages available * ``total``: total number of items in the list Sudo ==== If you have the administrator status, you can use ``sudo`` to act as another user. For example: .. code-block:: python p = gl.projects.create({'name': 'awesome_project'}, sudo='user1') Advanced HTTP configuration =========================== python-gitlab relies on ``requests`` ``Session`` objects to perform all the 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 ------------------- The following sample illustrates how to define a proxy configuration when using python-gitlab: .. code-block:: python import gitlab import requests session = requests.Session() session.proxies = { 'https': os.environ.get('https_proxy'), 'http': os.environ.get('http_proxy'), } gl = gitlab.gitlab(url, token, api_version=4, session=session) Reference: http://docs.python-requests.org/en/master/user/advanced/#proxies Client side certificate ----------------------- The following sample illustrates how to use a client-side certificate: .. code-block:: python import gitlab import requests session = requests.Session() s.cert = ('/path/to/client.cert', '/path/to/client.key') gl = gitlab.gitlab(url, token, api_version=4, session=session) Reference: http://docs.python-requests.org/en/master/user/advanced/#client-side-certificates python-gitlab-1.3.0/docs/api/000077500000000000000000000000001324224150200157705ustar00rootroot00000000000000python-gitlab-1.3.0/docs/api/gitlab.rst000066400000000000000000000021701324224150200177640ustar00rootroot00000000000000gitlab package ============== Subpackages ----------- .. toctree:: gitlab.v3 gitlab.v4 Submodules ---------- gitlab.base module ------------------ .. automodule:: gitlab.base :members: :undoc-members: :show-inheritance: gitlab.cli module ----------------- .. automodule:: gitlab.cli :members: :undoc-members: :show-inheritance: gitlab.config module -------------------- .. automodule:: gitlab.config :members: :undoc-members: :show-inheritance: gitlab.const module ------------------- .. automodule:: gitlab.const :members: :undoc-members: :show-inheritance: gitlab.exceptions module ------------------------ .. automodule:: gitlab.exceptions :members: :undoc-members: :show-inheritance: gitlab.mixins module -------------------- .. automodule:: gitlab.mixins :members: :undoc-members: :show-inheritance: gitlab.utils module ------------------- .. automodule:: gitlab.utils :members: :undoc-members: :show-inheritance: Module contents --------------- .. automodule:: gitlab :members: :undoc-members: :show-inheritance: python-gitlab-1.3.0/docs/api/gitlab.v3.rst000066400000000000000000000005001324224150200203060ustar00rootroot00000000000000gitlab.v3 package ================= Submodules ---------- gitlab.v3.objects module ------------------------ .. automodule:: gitlab.v3.objects :members: :undoc-members: :show-inheritance: Module contents --------------- .. automodule:: gitlab.v3 :members: :undoc-members: :show-inheritance: python-gitlab-1.3.0/docs/api/gitlab.v4.rst000066400000000000000000000005001324224150200203070ustar00rootroot00000000000000gitlab.v4 package ================= Submodules ---------- gitlab.v4.objects module ------------------------ .. automodule:: gitlab.v4.objects :members: :undoc-members: :show-inheritance: Module contents --------------- .. automodule:: gitlab.v4 :members: :undoc-members: :show-inheritance: python-gitlab-1.3.0/docs/changelog.rst000066400000000000000000000000361324224150200176770ustar00rootroot00000000000000.. include:: ../ChangeLog.rst python-gitlab-1.3.0/docs/cli.rst000066400000000000000000000136401324224150200165240ustar00rootroot00000000000000#################### ``gitlab`` CLI usage #################### ``python-gitlab`` provides a :command:`gitlab` command-line tool to interact with GitLab servers. It uses a configuration file to define how to connect to the servers. .. _cli_configuration: Configuration ============= Files ----- ``gitlab`` looks up 2 configuration files by default: ``/etc/python-gitlab.cfg`` System-wide configuration file ``~/.python-gitlab.cfg`` User configuration file You can use a different configuration file with the ``--config-file`` option. Content ------- The configuration file uses the ``INI`` format. It contains at least a ``[global]`` section, and a specific section for each GitLab server. For example: .. code-block:: ini [global] default = somewhere ssl_verify = true timeout = 5 [somewhere] url = https://some.whe.re private_token = vTbFeqJYCY3sibBP7BZM api_version = 3 [elsewhere] url = http://else.whe.re:8080 private_token = CkqsjqcQSFH5FQKDccu4 timeout = 1 The ``default`` option of the ``[global]`` section defines the GitLab server to use if no server is explicitly specified with the ``--gitlab`` CLI option. The ``[global]`` section also defines the values for the default connection parameters. You can override the values in each GitLab server section. .. list-table:: Global options :header-rows: 1 * - Option - Possible values - Description * - ``ssl_verify`` - ``True``, ``False``, or a ``str`` - Verify the SSL certificate. Set to ``False`` to disable verification, though this will create warnings. Any other value is interpreted as path to a CA_BUNDLE file or directory with certificates of trusted CAs. * - ``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. Only one of ``private_token`` or ``oauth_token`` should be defined. If neither are defined an anonymous request will be sent to the Gitlab server, with very limited permissions. .. list-table:: GitLab server options :header-rows: 1 * - Option - Description * - ``url`` - URL for the GitLab server * - ``private_token`` - Your user token. Login/password is not supported. Refer to `the official documentation`__ to learn how to obtain a token. * - ``oauth_token`` - 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 ``4`` since version 1.3.0. * - ``http_username`` - Username for optional HTTP authentication * - ``http_password`` - Password for optional HTTP authentication __ https://docs.gitlab.com/ce/user/profile/personal_access_tokens.html CLI === Objects and actions ------------------- The ``gitlab`` command expects two mandatory arguments. The first one is the type of object that you want to manipulate. The second is the action that you want to perform. For example: .. code-block:: console $ gitlab project list Use the ``--help`` option to list the available object types and actions: .. code-block:: console $ gitlab --help $ gitlab project --help Some actions require additional parameters. Use the ``--help`` option to list mandatory and optional arguments for an action: .. code-block:: console $ gitlab project create --help Optional arguments ------------------ Use the following optional arguments to change the behavior of ``gitlab``. These options must be defined before the mandatory arguments. ``--verbose``, ``-v`` Outputs detail about retrieved objects. Available for legacy (default) output only. ``--config-file``, ``-c`` Path to a configuration file. ``--gitlab``, ``-g`` ID of a GitLab server defined in the configuration file. ``--output``, ``-o`` Output format. Defaults to a custom format. Can also be ``yaml`` or ``json``. ``--fields``, ``-f`` Comma-separated list of fields to display (``yaml`` and ``json`` output formats only). If not used, all the object fields are displayed. Example: .. code-block:: console $ gitlab -o yaml -f id,permissions -g elsewhere -c /tmp/gl.cfg project list Examples ======== List the projects (paginated): .. code-block:: console $ gitlab project list List all the projects: .. code-block:: console $ gitlab project list --all Limit to 5 items per request, display the 1st page only .. code-block:: console $ gitlab project list --page 1 --per-page 5 Get a specific project (id 2): .. code-block:: console $ gitlab project get --id 2 Get a specific user by id: .. code-block:: console $ gitlab user get --id 3 Get a list of snippets for this project: .. code-block:: console $ gitlab project-issue list --project-id 2 Delete a snippet (id 3): .. code-block:: console $ gitlab project-snippet delete --id 3 --project-id 2 Update a snippet: .. code-block:: console $ gitlab project-snippet update --id 4 --project-id 2 \ --code "My New Code" Create a snippet: .. code-block:: console $ gitlab project-snippet create --project-id 2 Impossible to create object (Missing attribute(s): title, file-name, code) $ # oops, let's add the attributes: $ gitlab project-snippet create --project-id 2 --title "the title" \ --file-name "the name" --code "the code" Define the status of a commit (as would be done from a CI tool for example): .. code-block:: console $ gitlab project-commit-status create --project-id 2 \ --commit-id a43290c --state success --name ci/jenkins \ --target-url http://server/build/123 \ --description "Jenkins build succeeded" Use sudo to act as another user (admin only): .. code-block:: console $ gitlab project create --name user_project1 --sudo username python-gitlab-1.3.0/docs/conf.py000066400000000000000000000212551324224150200165230ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # python-gitlab documentation build configuration file, created by # sphinx-quickstart on Mon Dec 8 15:17:39 2014. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. from __future__ import unicode_literals import os import sys import sphinx sys.path.append('../') sys.path.append(os.path.dirname(__file__)) import gitlab on_rtd = os.environ.get('READTHEDOCS', None) == 'True' # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath('..')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.autosummary', 'ext.docstrings' ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'python-gitlab' copyright = '2013-2016, Gauvain Pocentek, Mika Mäenpää' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = gitlab.__version__ # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' if not on_rtd: # only import and set the theme if we're building docs locally try: import sphinx_rtd_theme html_theme = 'sphinx_rtd_theme' html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] except ImportError: # Theme not found, use default pass # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". #html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'python-gitlabdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'python-gitlab.tex', 'python-gitlab Documentation', 'Gauvain Pocentek, Mika Mäenpää', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'python-gitlab', 'python-gitlab Documentation', ['Gauvain Pocentek, Mika Mäenpää'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'python-gitlab', 'python-gitlab Documentation', 'Gauvain Pocentek, Mika Mäenpää', 'python-gitlab', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False python-gitlab-1.3.0/docs/ext/000077500000000000000000000000001324224150200160175ustar00rootroot00000000000000python-gitlab-1.3.0/docs/ext/__init__.py000066400000000000000000000000001324224150200201160ustar00rootroot00000000000000python-gitlab-1.3.0/docs/ext/docstrings.py000066400000000000000000000052551324224150200205570ustar00rootroot00000000000000import inspect import itertools import os import jinja2 import six import sphinx import sphinx.ext.napoleon as napoleon from sphinx.ext.napoleon.docstring import GoogleDocstring def classref(value, short=True): return value if not inspect.isclass(value): return ':class:%s' % value tilde = '~' if short else '' string = '%s.%s' % (value.__module__, value.__name__) return ':class:`%sgitlab.objects.%s`' % (tilde, value.__name__) def setup(app): app.connect('autodoc-process-docstring', _process_docstring) app.connect('autodoc-skip-member', napoleon._skip_member) conf = napoleon.Config._config_values for name, (default, rebuild) in six.iteritems(conf): app.add_config_value(name, default, rebuild) return {'version': sphinx.__display_version__, 'parallel_read_safe': True} def _process_docstring(app, what, name, obj, options, lines): result_lines = lines docstring = GitlabDocstring(result_lines, app.config, app, what, name, obj, options) result_lines = docstring.lines() lines[:] = result_lines[:] class GitlabDocstring(GoogleDocstring): def _build_doc(self, tmpl, **kwargs): env = jinja2.Environment(loader=jinja2.FileSystemLoader( os.path.dirname(__file__)), trim_blocks=False) env.filters['classref'] = classref template = env.get_template(tmpl) output = template.render(**kwargs) return output.split('\n') def __init__(self, docstring, config=None, app=None, what='', name='', obj=None, options=None): super(GitlabDocstring, self).__init__(docstring, config, app, what, name, obj, options) if name and name.startswith('gitlab.v4.objects'): return if getattr(self._obj, '__name__', None) == 'Gitlab': mgrs = [] gl = self._obj('http://dummy', private_token='dummy') for item in vars(gl).items(): if hasattr(item[1], 'obj_cls'): mgrs.append(item) self._parsed_lines.extend(self._build_doc('gl_tmpl.j2', mgrs=sorted(mgrs))) # BaseManager elif hasattr(self._obj, 'obj_cls') and self._obj.obj_cls is not None: self._parsed_lines.extend(self._build_doc('manager_tmpl.j2', cls=self._obj.obj_cls)) # GitlabObject elif hasattr(self._obj, 'canUpdate') and self._obj.canUpdate: self._parsed_lines.extend(self._build_doc('object_tmpl.j2', obj=self._obj)) python-gitlab-1.3.0/docs/ext/gl_tmpl.j2000066400000000000000000000002341324224150200177110ustar00rootroot00000000000000{% for attr, mgr in mgrs %} .. attribute:: {{ attr }} {{ mgr.__class__ | classref() }} manager for {{ mgr.obj_cls | classref() }} objects. {% endfor %} python-gitlab-1.3.0/docs/ext/manager_tmpl.j2000066400000000000000000000043331324224150200207250ustar00rootroot00000000000000Manager for {{ cls | classref() }} objects. {% if cls.canUpdate %} {{ cls | classref() }} objects can be updated. {% else %} {{ cls | classref() }} objects **cannot** be updated. {% endif %} {% if cls.canList %} .. method:: list(**kwargs) Returns a list of objects of type {{ cls | classref() }}. Available keys for ``kwargs`` are: {% for k in cls.requiredListAttrs %} * ``{{ k }}`` (required) {% endfor %} {% for k in cls.optionalListAttrs %} * ``{{ k }}`` (optional) {% endfor %} * ``per_page`` (int): number of item per page. May be limited by the server. * ``page`` (int): page to retrieve * ``all`` (bool): iterate over all the pages and return all the entries * ``sudo`` (string or int): run the request as another user (requires admin permissions) {% endif %} {% if cls.canGet %} {% if cls.getRequiresId %} .. method:: get(id, **kwargs) Get a single object of type {{ cls | classref() }} using its ``id``. {% else %} .. method:: get(**kwargs) Get a single object of type {{ cls | classref() }}. {% endif %} Available keys for ``kwargs`` are: {% for k in cls.requiredGetAttrs %} * ``{{ k }}`` (required) {% endfor %} {% for k in cls.optionalGetAttrs %} * ``{{ k }}`` (optional) {% endfor %} * ``sudo`` (string or int): run the request as another user (requires admin permissions) {% endif %} {% if cls.canCreate %} .. method:: create(data, **kwargs) Create an object of type {{ cls | classref() }}. ``data`` is a dict defining the object attributes. Available attributes are: {% for a in cls.requiredUrlAttrs %} * ``{{ a }}`` (required if not discovered on the parent objects) {% endfor %} {% for a in cls.requiredCreateAttrs %} * ``{{ a }}`` (required) {% endfor %} {% for a in cls.optionalCreateAttrs %} * ``{{ a }}`` (optional) {% endfor %} Available keys for ``kwargs`` are: * ``sudo`` (string or int): run the request as another user (requires admin permissions) {% endif %} {% if cls.canDelete %} .. method:: delete(id, **kwargs) Delete the object with ID ``id``. Available keys for ``kwargs`` are: * ``sudo`` (string or int): run the request as another user (requires admin permissions) {% endif %} python-gitlab-1.3.0/docs/ext/object_tmpl.j2000066400000000000000000000015301324224150200205550ustar00rootroot00000000000000{% for attr_name, cls, dummy in obj.managers %} .. attribute:: {{ attr_name }} {{ cls | classref() }} - Manager for {{ cls.obj_cls | classref() }} objects. {% endfor %} .. method:: save(**kwargs) Send the modified object to the GitLab server. The following attributes are sent: {% if obj.requiredUpdateAttrs or obj.optionalUpdateAttrs %} {% for a in obj.requiredUpdateAttrs %} * ``{{ a }}`` (required) {% endfor %} {% for a in obj.optionalUpdateAttrs %} * ``{{ a }}`` (optional) {% endfor %} {% else %} {% for a in obj.requiredCreateAttrs %} * ``{{ a }}`` (required) {% endfor %} {% for a in obj.optionalCreateAttrs %} * ``{{ a }}`` (optional) {% endfor %} {% endif %} Available keys for ``kwargs`` are: * ``sudo`` (string or int): run the request as another user (requires admin permissions) python-gitlab-1.3.0/docs/gl_objects/000077500000000000000000000000001324224150200173325ustar00rootroot00000000000000python-gitlab-1.3.0/docs/gl_objects/access_requests.py000066400000000000000000000010561324224150200231020ustar00rootroot00000000000000# list p_ars = project.accessrequests.list() g_ars = group.accessrequests.list() # end list # get p_ar = project.accessrequests.get(user_id) g_ar = group.accessrequests.get(user_id) # end get # create p_ar = project.accessrequests.create({}) g_ar = group.accessrequests.create({}) # end create # approve ar.approve() # defaults to DEVELOPER level ar.approve(access_level=gitlab.MASTER_ACCESS) # explicitly set access level # end approve # delete project.accessrequests.delete(user_id) group.accessrequests.delete(user_id) # or ar.delete() # end delete python-gitlab-1.3.0/docs/gl_objects/access_requests.rst000066400000000000000000000036011324224150200232600ustar00rootroot00000000000000############### Access requests ############### Users can request access to groups and projects. When access is granted the user should be given a numerical access level. The following constants are provided to represent the access levels: * ``gitlab.GUEST_ACCESS``: ``10`` * ``gitlab.REPORTER_ACCESS``: ``20`` * ``gitlab.DEVELOPER_ACCESS``: ``30`` * ``gitlab.MASTER_ACCESS``: ``40`` * ``gitlab.OWNER_ACCESS``: ``50`` References ---------- * v4 API: + :class:`gitlab.v4.objects.ProjectAccessRequest` + :class:`gitlab.v4.objects.ProjectAccessRequestManager` + :attr:`gitlab.v4.objects.Project.accessrequests` + :class:`gitlab.v4.objects.GroupAccessRequest` + :class:`gitlab.v4.objects.GroupAccessRequestManager` + :attr:`gitlab.v4.objects.Group.accessrequests` * v3 API: + :class:`gitlab.v3.objects.ProjectAccessRequest` + :class:`gitlab.v3.objects.ProjectAccessRequestManager` + :attr:`gitlab.v3.objects.Project.accessrequests` + :attr:`gitlab.Gitlab.project_accessrequests` + :class:`gitlab.v3.objects.GroupAccessRequest` + :class:`gitlab.v3.objects.GroupAccessRequestManager` + :attr:`gitlab.v3.objects.Group.accessrequests` + :attr:`gitlab.Gitlab.group_accessrequests` * GitLab API: https://docs.gitlab.com/ce/api/access_requests.html Examples -------- List access requests from projects and groups: .. literalinclude:: access_requests.py :start-after: # list :end-before: # end list Get a single request: .. literalinclude:: access_requests.py :start-after: # get :end-before: # end get Create an access request: .. literalinclude:: access_requests.py :start-after: # create :end-before: # end create Approve an access request: .. literalinclude:: access_requests.py :start-after: # approve :end-before: # end approve Deny (delete) an access request: .. literalinclude:: access_requests.py :start-after: # delete :end-before: # end delete python-gitlab-1.3.0/docs/gl_objects/branches.py000066400000000000000000000015721324224150200214760ustar00rootroot00000000000000# list branches = project.branches.list() # end list # get branch = project.branches.get('master') # end get # create # v4 branch = project.branches.create({'branch': 'feature1', 'ref': 'master'}) #v3 branch = project.branches.create({'branch_name': 'feature1', 'ref': 'master'}) # end create # delete project.branches.delete('feature1') # or branch.delete() # end delete # protect branch.protect() branch.unprotect() # end protect # p_branch list p_branches = project.protectedbranches.list() # end p_branch list # p_branch get p_branch = project.protectedbranches.get('master') # end p_branch get # p_branch create p_branch = project.protectedbranches.create({'name': '*-stable'}) # end p_branch create # p_branch delete project.protectedbranches.delete('*-stable') # or p_branch.delete() # end p_branch delete python-gitlab-1.3.0/docs/gl_objects/branches.rst000066400000000000000000000024731324224150200216570ustar00rootroot00000000000000######## Branches ######## References ---------- * v4 API: + :class:`gitlab.v4.objects.ProjectBranch` + :class:`gitlab.v4.objects.ProjectBranchManager` + :attr:`gitlab.v4.objects.Project.branches` * v3 API: + :class:`gitlab.v3.objects.ProjectBranch` + :class:`gitlab.v3.objects.ProjectBranchManager` + :attr:`gitlab.v3.objects.Project.branches` * GitLab API: https://docs.gitlab.com/ce/api/branches.html Examples -------- Get the list of branches for a repository: .. literalinclude:: branches.py :start-after: # list :end-before: # end list Get a single repository branch: .. literalinclude:: branches.py :start-after: # get :end-before: # end get Create a repository branch: .. literalinclude:: branches.py :start-after: # create :end-before: # end create Delete a repository branch: .. literalinclude:: branches.py :start-after: # delete :end-before: # end delete Protect/unprotect a repository branch: .. literalinclude:: branches.py :start-after: # protect :end-before: # end protect .. note:: By default, developers are not authorized to push or merge into protected branches. This can be changed by passing ``developers_can_push`` or ``developers_can_merge``: .. code-block:: python branch.protect(developers_can_push=True, developers_can_merge=True) python-gitlab-1.3.0/docs/gl_objects/builds.py000066400000000000000000000047511324224150200211750ustar00rootroot00000000000000# var list p_variables = project.variables.list() g_variables = group.variables.list() # end var list # var get p_var = project.variables.get('key_name') g_var = group.variables.get('key_name') # end var get # var create var = project.variables.create({'key': 'key1', 'value': 'value1'}) var = group.variables.create({'key': 'key1', 'value': 'value1'}) # end var create # var update var.value = 'new_value' var.save() # end var update # var delete project.variables.delete('key_name') group.variables.delete('key_name') # or var.delete() # end var delete # trigger list triggers = project.triggers.list() # end trigger list # trigger get trigger = project.triggers.get(trigger_token) # end trigger get # trigger create trigger = project.triggers.create({}) # v3 trigger = project.triggers.create({'description': 'mytrigger'}) # v4 # end trigger create # trigger delete project.triggers.delete(trigger_token) # or trigger.delete() # end trigger delete # list builds = project.builds.list() # v3 jobs = project.jobs.list() # v4 # end list # commit list # v3 only commit = gl.project_commits.get(commit_sha, project_id=1) builds = commit.builds() # end commit list # 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 job # artifacts build_or_job.artifacts() # end artifacts # stream artifacts with class class Foo(object): def __init__(self): self._fd = open('artifacts.zip', 'wb') def __call__(self, chunk): self._fd.write(chunk) target = Foo() build_or_job.artifacts(streamed=True, action=target) del(target) # flushes data on disk # end stream artifacts with class # stream artifacts with unzip zipfn = "___artifacts.zip" with open(zipfn, "wb") as f: build_or_job.artifacts(streamed=True, action=f.write) subprocess.run(["unzip", "-bo", zipfn]) os.unlink(zipfn) # end stream artifacts with unzip # keep artifacts build_or_job.keep_artifacts() # end keep artifacts # trace build_or_job.trace() # end trace # retry build_or_job.cancel() build_or_job.retry() # end retry # erase build_or_job.erase() # end erase # play build_or_job.play() # end play # trigger run project.trigger_build('master', trigger_token, {'extra_var1': 'foo', 'extra_var2': 'bar'}) # end trigger run python-gitlab-1.3.0/docs/gl_objects/builds.rst000066400000000000000000000174001324224150200213500ustar00rootroot00000000000000########################## Pipelines, Builds and Jobs ########################## Build and job are two classes representing the same object. Builds are used in v3 API, jobs in v4 API. Project pipelines ================= A pipeline is a group of jobs executed by GitLab CI. Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectPipeline` + :class:`gitlab.v4.objects.ProjectPipelineManager` + :attr:`gitlab.v4.objects.Project.pipelines` * v3 API: + :class:`gitlab.v3.objects.ProjectPipeline` + :class:`gitlab.v3.objects.ProjectPipelineManager` + :attr:`gitlab.v3.objects.Project.pipelines` + :attr:`gitlab.Gitlab.project_pipelines` * GitLab API: https://docs.gitlab.com/ce/api/pipelines.html Examples -------- List pipelines for a project:: pipelines = project.pipelines.list() Get a pipeline for a project:: pipeline = project.pipelines.get(pipeline_id) Create a pipeline for a particular reference:: pipeline = project.pipelines.create({'ref': 'master'}) Retry the failed builds for a pipeline:: pipeline.retry() Cancel builds in a pipeline:: pipeline.cancel() Triggers ======== Triggers provide a way to interact with the GitLab CI. Using a trigger a user or an application can run a new build/job for a specific commit. Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectTrigger` + :class:`gitlab.v4.objects.ProjectTriggerManager` + :attr:`gitlab.v4.objects.Project.triggers` * v3 API: + :class:`gitlab.v3.objects.ProjectTrigger` + :class:`gitlab.v3.objects.ProjectTriggerManager` + :attr:`gitlab.v3.objects.Project.triggers` + :attr:`gitlab.Gitlab.project_triggers` * GitLab API: https://docs.gitlab.com/ce/api/pipeline_triggers.html Examples -------- List triggers: .. literalinclude:: builds.py :start-after: # trigger list :end-before: # end trigger list Get a trigger: .. literalinclude:: builds.py :start-after: # trigger get :end-before: # end trigger get Create a trigger: .. literalinclude:: builds.py :start-after: # trigger create :end-before: # end trigger create Remove a trigger: .. literalinclude:: builds.py :start-after: # trigger delete :end-before: # end trigger delete Pipeline schedule ================= You can schedule pipeline runs using a cron-like syntax. Variables can be associated with the scheduled pipelines. Reference --------- * v4 API + :class:`gitlab.v4.objects.ProjectPipelineSchedule` + :class:`gitlab.v4.objects.ProjectPipelineScheduleManager` + :attr:`gitlab.v4.objects.Project.pipelineschedules` + :class:`gitlab.v4.objects.ProjectPipelineScheduleVariable` + :class:`gitlab.v4.objects.ProjectPipelineScheduleVariableManager` + :attr:`gitlab.v4.objects.Project.pipelineschedules` * GitLab API: https://docs.gitlab.com/ce/api/pipeline_schedules.html Examples -------- List pipeline schedules:: scheds = project.pipelineschedules.list() Get a single schedule:: sched = projects.pipelineschedules.get(schedule_id) Create a new schedule:: sched = project.pipelineschedules.create({ 'ref': 'master', 'description': 'Daily test', 'cron': '0 1 * * *'}) Update a schedule:: sched.cron = '1 2 * * *' sched.save() Delete a schedule:: sched.delete() Create a schedule variable:: var = sched.variables.create({'key': 'foo', 'value': 'bar'}) Edit a schedule variable:: var.value = 'new_value' var.save() Delete a schedule variable:: var.delete() Projects and groups variables ============================= You can associate variables to projects and groups to modify the build/job scripts behavior. Reference --------- * v4 API + :class:`gitlab.v4.objects.ProjectVariable` + :class:`gitlab.v4.objects.ProjectVariableManager` + :attr:`gitlab.v4.objects.Project.variables` + :class:`gitlab.v4.objects.GroupVariable` + :class:`gitlab.v4.objects.GroupVariableManager` + :attr:`gitlab.v4.objects.Group.variables` * v3 API + :class:`gitlab.v3.objects.ProjectVariable` + :class:`gitlab.v3.objects.ProjectVariableManager` + :attr:`gitlab.v3.objects.Project.variables` + :attr:`gitlab.Gitlab.project_variables` * GitLab API + https://docs.gitlab.com/ce/api/project_level_variables.html + https://docs.gitlab.com/ce/api/group_level_variables.html Examples -------- List variables: .. literalinclude:: builds.py :start-after: # var list :end-before: # end var list Get a variable: .. literalinclude:: builds.py :start-after: # var get :end-before: # end var get Create a variable: .. literalinclude:: builds.py :start-after: # var create :end-before: # end var create Update a variable value: .. literalinclude:: builds.py :start-after: # var update :end-before: # end var update Remove a variable: .. literalinclude:: builds.py :start-after: # var delete :end-before: # end var delete Builds/Jobs =========== 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 --------- * v4 API + :class:`gitlab.v4.objects.ProjectJob` + :class:`gitlab.v4.objects.ProjectJobManager` + :attr:`gitlab.v4.objects.Project.jobs` * v3 API + :class:`gitlab.v3.objects.ProjectJob` + :class:`gitlab.v3.objects.ProjectJobManager` + :attr:`gitlab.v3.objects.Project.jobs` + :attr:`gitlab.Gitlab.project_jobs` * GitLab API: https://docs.gitlab.com/ce/api/jobs.html Examples -------- Jobs are usually automatically triggered, but you can explicitly trigger a new job: Trigger a new job on a project: .. literalinclude:: builds.py :start-after: # trigger run :end-before: # end trigger run List jobs for the project: .. literalinclude:: builds.py :start-after: # list :end-before: # end list To list builds for a specific commit, create a :class:`~gitlab.v3.objects.ProjectCommit` object and use its :attr:`~gitlab.v3.objects.ProjectCommit.builds` method (v3 only): .. literalinclude:: builds.py :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 job :end-before: # end get job Get a job artifact: .. literalinclude:: builds.py :start-after: # artifacts :end-before: # end artifacts .. warning:: Artifacts are entirely stored in memory in this example. .. _streaming_example: You can download artifacts as a stream. Provide a callable to handle the stream: .. literalinclude:: builds.py :start-after: # stream artifacts with class :end-before: # end stream artifacts with class In this second example, you can directly stream the output into a file, and unzip it afterwards: .. literalinclude:: builds.py :start-after: # stream artifacts with unzip :end-before: # end stream artifacts with unzip Mark a job artifact as kept when expiration is set: .. literalinclude:: builds.py :start-after: # keep artifacts :end-before: # end keep artifacts Get a job trace: .. literalinclude:: builds.py :start-after: # trace :end-before: # end trace .. warning:: Traces are entirely stored in memory unless you use the streaming feature. See :ref:`the artifacts example `. Cancel/retry a job: .. literalinclude:: builds.py :start-after: # retry :end-before: # end retry Play (trigger) a job: .. literalinclude:: builds.py :start-after: # play :end-before: # end play Erase a job (artifacts and trace): .. literalinclude:: builds.py :start-after: # erase :end-before: # end erase python-gitlab-1.3.0/docs/gl_objects/commits.py000066400000000000000000000026261324224150200213650ustar00rootroot00000000000000# list commits = project.commits.list() # end list # filter list commits = project.commits.list(ref_name='my_branch') 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', # v3 'branch': 'master', # v4 'commit_message': 'blah blah blah', 'actions': [ { 'action': 'create', 'file_path': 'blah', 'content': 'blah' } ] } commit = project.commits.create(data) # end create # get commit = project.commits.get('e3d5a71b') # end get # diff diff = commit.diff() # end diff # cherry commit.cherry_pick(branch='target_branch') # end cherry # comments list comments = commit.comments.list() # end comments list # comments create # Global comment commit = commit.comments.create({'note': 'This is a nice comment'}) # Comment on a line in a file (on the new version of the file) commit = commit.comments.create({'note': 'This is another comment', 'line': 12, 'line_type': 'new', 'path': 'README.rst'}) # end comments create # statuses list statuses = commit.statuses.list() # end statuses list # statuses set commit.statuses.create({'state': 'success'}) # end statuses set python-gitlab-1.3.0/docs/gl_objects/commits.rst000066400000000000000000000056641324224150200215520ustar00rootroot00000000000000####### Commits ####### Commits ======= Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectCommit` + :class:`gitlab.v4.objects.ProjectCommitManager` + :attr:`gitlab.v4.objects.Project.commits` * v3 API: + :class:`gitlab.v3.objects.ProjectCommit` + :class:`gitlab.v3.objects.ProjectCommitManager` + :attr:`gitlab.v3.objects.Project.commits` + :attr:`gitlab.Gitlab.project_commits` * 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 -------- List the commits for a project: .. literalinclude:: commits.py :start-after: # list :end-before: # end list You can use the ``ref_name``, ``since`` and ``until`` filters to limit the results: .. literalinclude:: commits.py :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 :start-after: # get :end-before: # end get Get the diff for a commit: .. literalinclude:: commits.py :start-after: # diff :end-before: # end diff Cherry-pick a commit into another branch: .. literalinclude:: commits.py :start-after: # cherry :end-before: # end cherry Commit comments =============== Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectCommitComment` + :class:`gitlab.v4.objects.ProjectCommitCommentManager` + :attr:`gitlab.v4.objects.Commit.comments` * v3 API: + :class:`gitlab.v3.objects.ProjectCommit` + :class:`gitlab.v3.objects.ProjectCommitManager` + :attr:`gitlab.v3.objects.Commit.comments` + :attr:`gitlab.v3.objects.Project.commit_comments` + :attr:`gitlab.Gitlab.project_commit_comments` * GitLab API: https://docs.gitlab.com/ce/api/commits.html Examples -------- Get the comments for a commit: .. literalinclude:: commits.py :start-after: # comments list :end-before: # end comments list Add a comment on a commit: .. literalinclude:: commits.py :start-after: # comments create :end-before: # end comments create Commit status ============= Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectCommitStatus` + :class:`gitlab.v4.objects.ProjectCommitStatusManager` + :attr:`gitlab.v4.objects.Commit.statuses` * v3 API: + :class:`gitlab.v3.objects.ProjectCommit` + :class:`gitlab.v3.objects.ProjectCommitManager` + :attr:`gitlab.v3.objects.Commit.statuses` + :attr:`gitlab.v3.objects.Project.commit_statuses` + :attr:`gitlab.Gitlab.project_commit_statuses` * GitLab API: https://docs.gitlab.com/ce/api/commits.html Examples -------- Get the statuses for a commit: .. literalinclude:: commits.py :start-after: # statuses list :end-before: # end statuses list Change the status of a commit: .. literalinclude:: commits.py :start-after: # statuses set :end-before: # end statuses set python-gitlab-1.3.0/docs/gl_objects/deploy_keys.py000066400000000000000000000011021324224150200222250ustar00rootroot00000000000000# global list keys = gl.deploykeys.list() # end global list # global get key = gl.deploykeys.get(key_id) # end global get # list keys = project.keys.list() # end list # get key = project.keys.get(key_id) # end get # create key = project.keys.create({'title': 'jenkins key', 'key': open('/home/me/.ssh/id_rsa.pub').read()}) # end create # delete key = project.keys.list(key_id) # or key.delete() # end delete # enable project.keys.enable(key_id) # end enable # disable project_key.delete() # v4 project.keys.disable(key_id) # v3 # end disable python-gitlab-1.3.0/docs/gl_objects/deploy_keys.rst000066400000000000000000000036511324224150200224200ustar00rootroot00000000000000########### Deploy keys ########### Deploy keys =========== Reference --------- * v4 API: + :class:`gitlab.v4.objects.DeployKey` + :class:`gitlab.v4.objects.DeployKeyManager` + :attr:`gitlab.Gitlab.deploykeys` * v3 API: + :class:`gitlab.v3.objects.DeployKey` + :class:`gitlab.v3.objects.DeployKeyManager` + :attr:`gitlab.Gitlab.deploykeys` * GitLab API: https://docs.gitlab.com/ce/api/deploy_keys.html Examples -------- List the deploy keys: .. literalinclude:: deploy_keys.py :start-after: # global list :end-before: # end global list Get a single deploy key: .. literalinclude:: deploy_keys.py :start-after: # global get :end-before: # end global get Deploy keys for projects ======================== Deploy keys can be managed on a per-project basis. Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectKey` + :class:`gitlab.v4.objects.ProjectKeyManager` + :attr:`gitlab.v4.objects.Project.keys` * v3 API: + :class:`gitlab.v3.objects.ProjectKey` + :class:`gitlab.v3.objects.ProjectKeyManager` + :attr:`gitlab.v3.objects.Project.keys` + :attr:`gitlab.Gitlab.project_keys` * GitLab API: https://docs.gitlab.com/ce/api/deploy_keys.html Examples -------- List keys for a project: .. literalinclude:: deploy_keys.py :start-after: # list :end-before: # end list Get a single deploy key: .. literalinclude:: deploy_keys.py :start-after: # get :end-before: # end get Create a deploy key for a project: .. literalinclude:: deploy_keys.py :start-after: # create :end-before: # end create Delete a deploy key for a project: .. literalinclude:: deploy_keys.py :start-after: # delete :end-before: # end delete Enable a deploy key for a project: .. literalinclude:: deploy_keys.py :start-after: # enable :end-before: # end enable Disable a deploy key for a project: .. literalinclude:: deploy_keys.py :start-after: # disable :end-before: # end disable python-gitlab-1.3.0/docs/gl_objects/deployments.py000066400000000000000000000002001324224150200222370ustar00rootroot00000000000000# list deployments = project.deployments.list() # end list # get deployment = project.deployments.get(deployment_id) # end get python-gitlab-1.3.0/docs/gl_objects/deployments.rst000066400000000000000000000013561324224150200224340ustar00rootroot00000000000000########### Deployments ########### Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectDeployment` + :class:`gitlab.v4.objects.ProjectDeploymentManager` + :attr:`gitlab.v4.objects.Project.deployments` * v3 API: + :class:`gitlab.v3.objects.ProjectDeployment` + :class:`gitlab.v3.objects.ProjectDeploymentManager` + :attr:`gitlab.v3.objects.Project.deployments` + :attr:`gitlab.Gitlab.project_deployments` * GitLab API: https://docs.gitlab.com/ce/api/deployments.html Examples -------- List deployments for a project: .. literalinclude:: deployments.py :start-after: # list :end-before: # end list Get a single deployment: .. literalinclude:: deployments.py :start-after: # get :end-before: # end get python-gitlab-1.3.0/docs/gl_objects/emojis.rst000066400000000000000000000022541324224150200213550ustar00rootroot00000000000000############ 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) python-gitlab-1.3.0/docs/gl_objects/environments.py000066400000000000000000000006431324224150200224360ustar00rootroot00000000000000# list environments = project.environments.list() # end list # get environment = project.environments.get(environment_id) # end get # create environment = project.environments.create({'name': 'production'}) # end create # update environment.external_url = 'http://foo.bar.com' environment.save() # end update # delete environment = project.environments.delete(environment_id) # or environment.delete() # end delete python-gitlab-1.3.0/docs/gl_objects/environments.rst000066400000000000000000000022031324224150200226100ustar00rootroot00000000000000############ Environments ############ Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectEnvironment` + :class:`gitlab.v4.objects.ProjectEnvironmentManager` + :attr:`gitlab.v4.objects.Project.environments` * v3 API: + :class:`gitlab.v3.objects.ProjectEnvironment` + :class:`gitlab.v3.objects.ProjectEnvironmentManager` + :attr:`gitlab.v3.objects.Project.environments` + :attr:`gitlab.Gitlab.project_environments` * GitLab API: https://docs.gitlab.com/ce/api/environments.html Examples -------- List environments for a project: .. literalinclude:: environments.py :start-after: # list :end-before: # end list Get a single environment: .. literalinclude:: environments.py :start-after: # get :end-before: # end get Create an environment for a project: .. literalinclude:: environments.py :start-after: # create :end-before: # end create Update an environment for a project: .. literalinclude:: environments.py :start-after: # update :end-before: # end update Delete an environment for a project: .. literalinclude:: environments.py :start-after: # delete :end-before: # end delete python-gitlab-1.3.0/docs/gl_objects/events.rst000066400000000000000000000023601324224150200213710ustar00rootroot00000000000000###### 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 `_. 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() python-gitlab-1.3.0/docs/gl_objects/features.rst000066400000000000000000000007071324224150200217060ustar00rootroot00000000000000############## 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) python-gitlab-1.3.0/docs/gl_objects/groups.rst000066400000000000000000000075111324224150200214070ustar00rootroot00000000000000###### Groups ###### Groups ====== Reference --------- * v4 API: + :class:`gitlab.v4.objects.Group` + :class:`gitlab.v4.objects.GroupManager` + :attr:`gitlab.Gitlab.groups` * v3 API: + :class:`gitlab.v3.objects.Group` + :class:`gitlab.v3.objects.GroupManager` + :attr:`gitlab.Gitlab.groups` * GitLab API: https://docs.gitlab.com/ce/api/groups.html Examples -------- List the groups:: groups = gl.groups.list() Get a group's detail:: group = gl.groups.get(group_id) List a group's projects:: projects = group.projects.list() You can filter and sort the result using the following parameters: * ``archived``: limit by archived status * ``visibility``: limit by visibility. Allowed values are ``public``, ``internal`` and ``private`` * ``search``: limit to groups matching the given value * ``order_by``: sort by criteria. Allowed values are ``id``, ``name``, ``path``, ``created_at``, ``updated_at`` and ``last_activity_at`` * ``sort``: sort order: ``asc`` or ``desc`` * ``ci_enabled_first``: return CI enabled groups first Create a group:: group = gl.groups.create({'name': 'group1', 'path': 'group1'}) Update a group:: group.description = 'My awesome group' group.save() Remove a group:: 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 ============= The following constants define the supported access levels: * ``gitlab.GUEST_ACCESS = 10`` * ``gitlab.REPORTER_ACCESS = 20`` * ``gitlab.DEVELOPER_ACCESS = 30`` * ``gitlab.MASTER_ACCESS = 40`` * ``gitlab.OWNER_ACCESS = 50`` Reference --------- * v4 API: + :class:`gitlab.v4.objects.GroupMember` + :class:`gitlab.v4.objects.GroupMemberManager` + :attr:`gitlab.v4.objects.Group.members` * v3 API: + :class:`gitlab.v3.objects.GroupMember` + :class:`gitlab.v3.objects.GroupMemberManager` + :attr:`gitlab.v3.objects.Group.members` + :attr:`gitlab.Gitlab.group_members` * GitLab API: https://docs.gitlab.com/ce/api/groups.html Examples -------- List group members:: members = group.members.list() Get a group member:: members = group.members.get(member_id) Add a member to the group:: member = group.members.create({'user_id': user_id, 'access_level': gitlab.GUEST_ACCESS}) Update a member (change the access level):: member.access_level = gitlab.DEVELOPER_ACCESS member.save() Remove a member from the group:: group.members.delete(member_id) # or member.delete() python-gitlab-1.3.0/docs/gl_objects/issues.py000066400000000000000000000046031324224150200212220ustar00rootroot00000000000000# list issues = gl.issues.list() # end list # filtered list open_issues = gl.issues.list(state='opened') closed_issues = gl.issues.list(state='closed') tagged_issues = gl.issues.list(labels=['foo', 'bar']) # end filtered list # group issues list issues = group.issues.list() # Filter using the state, labels and milestone parameters issues = group.issues.list(milestone='1.0', state='opened') # Order using the order_by and sort parameters issues = group.issues.list(order_by='created_at', sort='desc') # end group issues list # project issues list issues = project.issues.list() # Filter using the state, labels and milestone parameters issues = project.issues.list(milestone='1.0', state='opened') # Order using the order_by and sort parameters issues = project.issues.list(order_by='created_at', sort='desc') # end project issues list # project issues get 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.'}) # end project issues create # project issue update issue.labels = ['foo', 'bar'] issue.save() # end project issue update # project issue open_close # close an issue issue.state_event = 'close' issue.save() # reopen it issue.state_event = 'reopen' issue.save() # end project issue open_close # project issue delete project.issues.delete(issue_id) # pr issue.delete() # end project issue delete # project issue subscribe issue.subscribe() issue.unsubscribe() # end project issue subscribe # project issue move issue.move(new_project_id) # end project issue move # project issue todo issue.todo() # end project issue todo # project issue time tracking stats issue.time_stats() # end project issue time tracking stats # project issue set time estimate issue.set_time_estimate({'duration': '3h30m'}) # end project issue set time estimate # project issue reset time estimate issue.reset_time_estimate() # end project issue reset time estimate # project issue set time spent issue.add_time_spent({'duration': '3h30m'}) # end project issue set time spent # project issue reset time spent issue.reset_time_spent() # end project issue reset time spent # project issue useragent detail = issue.user_agent_detail() # end project issue useragent python-gitlab-1.3.0/docs/gl_objects/issues.rst000066400000000000000000000103011324224150200213720ustar00rootroot00000000000000###### Issues ###### Reported issues =============== Reference --------- * v4 API: + :class:`gitlab.v4.objects.Issue` + :class:`gitlab.v4.objects.IssueManager` + :attr:`gitlab.Gitlab.issues` * v3 API: + :class:`gitlab.v3.objects.Issue` + :class:`gitlab.v3.objects.IssueManager` + :attr:`gitlab.Gitlab.issues` * GitLab API: https://docs.gitlab.com/ce/api/issues.html Examples -------- List the issues: .. literalinclude:: issues.py :start-after: # list :end-before: # end list Use the ``state`` and ``label`` parameters to filter the results. Use the ``order_by`` and ``sort`` attributes to sort the results: .. literalinclude:: issues.py :start-after: # filtered list :end-before: # end filtered list Group issues ============ Reference --------- * v4 API: + :class:`gitlab.v4.objects.GroupIssue` + :class:`gitlab.v4.objects.GroupIssueManager` + :attr:`gitlab.v4.objects.Group.issues` * v3 API: + :class:`gitlab.v3.objects.GroupIssue` + :class:`gitlab.v3.objects.GroupIssueManager` + :attr:`gitlab.v3.objects.Group.issues` + :attr:`gitlab.Gitlab.group_issues` * GitLab API: https://docs.gitlab.com/ce/api/issues.html Examples -------- List the group issues: .. literalinclude:: issues.py :start-after: # group issues list :end-before: # end group issues list Project issues ============== Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectIssue` + :class:`gitlab.v4.objects.ProjectIssueManager` + :attr:`gitlab.v4.objects.Project.issues` * v3 API: + :class:`gitlab.v3.objects.ProjectIssue` + :class:`gitlab.v3.objects.ProjectIssueManager` + :attr:`gitlab.v3.objects.Project.issues` + :attr:`gitlab.Gitlab.project_issues` * GitLab API: https://docs.gitlab.com/ce/api/issues.html Examples -------- List the project issues: .. literalinclude:: issues.py :start-after: # project issues list :end-before: # end project issues list Get a project issue: .. literalinclude:: issues.py :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 :start-after: # project issues create :end-before: # end project issues create Update an issue: .. literalinclude:: issues.py :start-after: # project issue update :end-before: # end project issue update Close / reopen an issue: .. literalinclude:: issues.py :start-after: # project issue open_close :end-before: # end project issue open_close Delete an issue: .. literalinclude:: issues.py :start-after: # project issue delete :end-before: # end project issue delete Subscribe / unsubscribe from an issue: .. literalinclude:: issues.py :start-after: # project issue subscribe :end-before: # end project issue subscribe Move an issue to another project: .. literalinclude:: issues.py :start-after: # project issue move :end-before: # end project issue move Make an issue as todo: .. literalinclude:: issues.py :start-after: # project issue todo :end-before: # end project issue todo Get time tracking stats: .. literalinclude:: issues.py :start-after: # project issue time tracking stats :end-before: # end project issue time tracking stats Set a time estimate for an issue: .. literalinclude:: issues.py :start-after: # project issue set time estimate :end-before: # end project issue set time estimate Reset a time estimate for an issue: .. literalinclude:: issues.py :start-after: # project issue reset time estimate :end-before: # end project issue reset time estimate Add spent time for an issue: .. literalinclude:: issues.py :start-after: # project issue set time spent :end-before: # end project issue set time spent Reset spent time for an issue: .. literalinclude:: issues.py :start-after: # project issue reset time spent :end-before: # end project issue reset time spent Get user agent detail for the issue (admin only): .. literalinclude:: issues.py :start-after: # project issue useragent :end-before: # end project issue useragent python-gitlab-1.3.0/docs/gl_objects/labels.py000066400000000000000000000013131324224150200211440ustar00rootroot00000000000000# list labels = project.labels.list() # end list # get label = project.labels.get(label_name) # end get # create label = project.labels.create({'name': 'foo', 'color': '#8899aa'}) # end create # update # change the name of the label: label.new_name = 'bar' label.save() # change its color: label.color = '#112233' label.save() # end update # delete project.labels.delete(label_id) # or label.delete() # end delete # use # Labels are defined as lists in issues and merge requests. The labels must # exist. issue = p.issues.create({'title': 'issue title', 'description': 'issue description', 'labels': ['foo']}) issue.labels.append('bar') issue.save() # end use python-gitlab-1.3.0/docs/gl_objects/labels.rst000066400000000000000000000022011324224150200213210ustar00rootroot00000000000000###### Labels ###### Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectLabel` + :class:`gitlab.v4.objects.ProjectLabelManager` + :attr:`gitlab.v4.objects.Project.labels` * v3 API: + :class:`gitlab.v3.objects.ProjectLabel` + :class:`gitlab.v3.objects.ProjectLabelManager` + :attr:`gitlab.v3.objects.Project.labels` + :attr:`gitlab.Gitlab.project_labels` * GitLab API: https://docs.gitlab.com/ce/api/labels.html Examples -------- List labels for a project: .. literalinclude:: labels.py :start-after: # list :end-before: # end list Get a single label: .. literalinclude:: labels.py :start-after: # get :end-before: # end get Create a label for a project: .. literalinclude:: labels.py :start-after: # create :end-before: # end create Update a label for a project: .. literalinclude:: labels.py :start-after: # update :end-before: # end update Delete a label for a project: .. literalinclude:: labels.py :start-after: # delete :end-before: # end delete Managing labels in issues and merge requests: .. literalinclude:: labels.py :start-after: # use :end-before: # end use python-gitlab-1.3.0/docs/gl_objects/messages.py000066400000000000000000000005461324224150200215200ustar00rootroot00000000000000# list msgs = gl.broadcastmessages.list() # end list # get msg = gl.broadcastmessages.get(msg_id) # end get # create msg = gl.broadcastmessages.create({'message': 'Important information'}) # end create # update msg.font = '#444444' msg.color = '#999999' msg.save() # end update # delete gl.broadcastmessages.delete(msg_id) # or msg.delete() # end delete python-gitlab-1.3.0/docs/gl_objects/messages.rst000066400000000000000000000023711324224150200216760ustar00rootroot00000000000000################## Broadcast messages ################## You can use broadcast messages to display information on all pages of the gitlab web UI. You must have administration permissions to manipulate broadcast messages. References ---------- * v4 API: + :class:`gitlab.v4.objects.BroadcastMessage` + :class:`gitlab.v4.objects.BroadcastMessageManager` + :attr:`gitlab.Gitlab.broadcastmessages` * v3 API: + :class:`gitlab.v3.objects.BroadcastMessage` + :class:`gitlab.v3.objects.BroadcastMessageManager` + :attr:`gitlab.Gitlab.broadcastmessages` * GitLab API: https://docs.gitlab.com/ce/api/broadcast_messages.html Examples -------- List the messages: .. literalinclude:: messages.py :start-after: # list :end-before: # end list Get a single message: .. literalinclude:: messages.py :start-after: # get :end-before: # end get Create a message: .. literalinclude:: messages.py :start-after: # create :end-before: # end create The date format for ``starts_at`` and ``ends_at`` parameters is ``YYYY-MM-ddThh:mm:ssZ``. Update a message: .. literalinclude:: messages.py :start-after: # update :end-before: # end update Delete a message: .. literalinclude:: messages.py :start-after: # delete :end-before: # end delete python-gitlab-1.3.0/docs/gl_objects/milestones.py000066400000000000000000000014451324224150200220720ustar00rootroot00000000000000# list p_milestones = project.milestones.list() g_milestones = group.milestones.list() # end list # filter p_milestones = project.milestones.list(state='closed') g_milestones = group.milestones.list(state='active') # end filter # get p_milestone = project.milestones.get(milestone_id) g_milestone = group.milestones.get(milestone_id) # end get # create milestone = project.milestones.create({'title': '1.0'}) # end create # update milestone.description = 'v 1.0 release' milestone.save() # end update # state # close a milestone milestone.state_event = 'close' milestone.save() # activate a milestone milestone.state_event = 'activate' milestone.save() # end state # issues issues = milestone.issues() # end issues # merge_requests merge_requests = milestone.merge_requests() # end merge_requests python-gitlab-1.3.0/docs/gl_objects/milestones.rst000066400000000000000000000035441324224150200222540ustar00rootroot00000000000000########## Milestones ########## Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectMilestone` + :class:`gitlab.v4.objects.ProjectMilestoneManager` + :attr:`gitlab.v4.objects.Project.milestones` + :class:`gitlab.v4.objects.GroupMilestone` + :class:`gitlab.v4.objects.GroupMilestoneManager` + :attr:`gitlab.v4.objects.Group.milestones` * v3 API: + :class:`gitlab.v3.objects.ProjectMilestone` + :class:`gitlab.v3.objects.ProjectMilestoneManager` + :attr:`gitlab.v3.objects.Project.milestones` + :attr:`gitlab.Gitlab.project_milestones` * GitLab API: + https://docs.gitlab.com/ce/api/milestones.html + https://docs.gitlab.com/ce/api/group_milestones.html Examples -------- List the milestones for a project or a group: .. literalinclude:: milestones.py :start-after: # list :end-before: # end list You can filter the list using the following parameters: * ``iid``: unique ID of the milestone for the project * ``state``: either ``active`` or ``closed`` * ``search``: to search using a string .. literalinclude:: milestones.py :start-after: # filter :end-before: # end filter Get a single milestone: .. literalinclude:: milestones.py :start-after: # get :end-before: # end get Create a milestone: .. literalinclude:: milestones.py :start-after: # create :end-before: # end create Edit a milestone: .. literalinclude:: milestones.py :start-after: # update :end-before: # end update Change the state of a milestone (activate / close): .. literalinclude:: milestones.py :start-after: # state :end-before: # end state List the issues related to a milestone: .. literalinclude:: milestones.py :start-after: # issues :end-before: # end issues List the merge requests related to a milestone: .. literalinclude:: milestones.py :start-after: # merge_requests :end-before: # end merge_requests python-gitlab-1.3.0/docs/gl_objects/mrs.py000066400000000000000000000021551324224150200205100ustar00rootroot00000000000000# list mrs = project.mergerequests.list() # end list # filtered list mrs = project.mergerequests.list(state='merged', order_by='updated_at') # end filtered list # get mr = project.mergerequests.get(mr_id) # end get # create mr = project.mergerequests.create({'source_branch': 'cool_feature', 'target_branch': 'master', 'title': 'merge cool feature', 'labels': ['label1', 'label2']}) # end create # update mr.description = 'New description' mr.labels = ['foo', 'bar'] mr.save() # end update # state mr.state_event = 'close' # or 'reopen' mr.save() # end state # delete project.mergerequests.delete(mr_id) # or mr.delete() # end delete # merge mr.merge() # end merge # cancel mr.cancel_merge_when_build_succeeds() # v3 mr.cancel_merge_when_pipeline_succeeds() # v4 # end cancel # issues mr.closes_issues() # end issues # subscribe mr.subscribe() mr.unsubscribe() # end subscribe # todo mr.todo() # end todo # diff list diffs = mr.diffs.list() # end diff list # diff get diff = mr.diffs.get(diff_id) # end diff get python-gitlab-1.3.0/docs/gl_objects/mrs.rst000066400000000000000000000051201324224150200206630ustar00rootroot00000000000000############## Merge requests ############## You can use merge requests to notify a project that a branch is ready for merging. The owner of the target projet can accept the merge request. The v3 API uses the ``id`` attribute to identify a merge request, the v4 API uses the ``iid`` attribute. Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectMergeRequest` + :class:`gitlab.v4.objects.ProjectMergeRequestManager` + :attr:`gitlab.v4.objects.Project.mergerequests` * v3 API: + :class:`gitlab.v3.objects.ProjectMergeRequest` + :class:`gitlab.v3.objects.ProjectMergeRequestManager` + :attr:`gitlab.v3.objects.Project.mergerequests` + :attr:`gitlab.Gitlab.project_mergerequests` * GitLab API: https://docs.gitlab.com/ce/api/merge_requests.html Examples -------- List MRs for a project: .. literalinclude:: mrs.py :start-after: # list :end-before: # end list You can filter and sort the returned list with the following parameters: * ``iid``: iid (unique ID for the project) of the MR (v3 API) * ``state``: state of the MR. It can be one of ``all``, ``merged``, ``opened`` or ``closed`` * ``order_by``: sort by ``created_at`` or ``updated_at`` * ``sort``: sort order (``asc`` or ``desc``) For example: .. literalinclude:: mrs.py :start-after: # list :end-before: # end list Get a single MR: .. literalinclude:: mrs.py :start-after: # get :end-before: # end get Create a MR: .. literalinclude:: mrs.py :start-after: # create :end-before: # end create Update a MR: .. literalinclude:: mrs.py :start-after: # update :end-before: # end update Change the state of a MR (close or reopen): .. literalinclude:: mrs.py :start-after: # state :end-before: # end state Delete a MR: .. literalinclude:: mrs.py :start-after: # delete :end-before: # end delete Accept a MR: .. literalinclude:: mrs.py :start-after: # merge :end-before: # end merge Cancel a MR when the build succeeds: .. literalinclude:: mrs.py :start-after: # cancel :end-before: # end cancel List issues that will close on merge: .. literalinclude:: mrs.py :start-after: # issues :end-before: # end issues Subscribe/unsubscribe a MR: .. literalinclude:: mrs.py :start-after: # subscribe :end-before: # end subscribe Mark a MR as todo: .. literalinclude:: mrs.py :start-after: # todo :end-before: # end todo List the diffs for a merge request: .. literalinclude:: mrs.py :start-after: # diff list :end-before: # end diff list Get a diff for a merge request: .. literalinclude:: mrs.py :start-after: # diff get :end-before: # end diff get python-gitlab-1.3.0/docs/gl_objects/namespaces.py000066400000000000000000000001711324224150200220220ustar00rootroot00000000000000# list namespaces = gl.namespaces.list() # end list # search namespaces = gl.namespaces.list(search='foo') # end search python-gitlab-1.3.0/docs/gl_objects/namespaces.rst000066400000000000000000000011611324224150200222020ustar00rootroot00000000000000########## Namespaces ########## Reference --------- * v4 API: + :class:`gitlab.v4.objects.Namespace` + :class:`gitlab.v4.objects.NamespaceManager` + :attr:`gitlab.Gitlab.namespaces` * v3 API: + :class:`gitlab.v3.objects.Namespace` + :class:`gitlab.v3.objects.NamespaceManager` + :attr:`gitlab.Gitlab.namespaces` * GitLab API: https://docs.gitlab.com/ce/api/namespaces.html Examples -------- List namespaces: .. literalinclude:: namespaces.py :start-after: # list :end-before: # end list Search namespaces: .. literalinclude:: namespaces.py :start-after: # search :end-before: # end search python-gitlab-1.3.0/docs/gl_objects/notifications.py000066400000000000000000000011021324224150200225470ustar00rootroot00000000000000# get # global settings settings = gl.notificationsettings.get() # for a group settings = gl.groups.get(group_id).notificationsettings.get() # for a project settings = gl.projects.get(project_id).notificationsettings.get() # end get # update # use a predefined level settings.level = gitlab.NOTIFICATION_LEVEL_WATCH # create a custom setup settings.level = gitlab.NOTIFICATION_LEVEL_CUSTOM settings.save() # will create additional attributes, but not mandatory settings.new_merge_request = True settings.new_issue = True settings.new_note = True settings.save() # end update python-gitlab-1.3.0/docs/gl_objects/notifications.rst000066400000000000000000000035751324224150200227470ustar00rootroot00000000000000##################### Notification settings ##################### You can define notification settings globally, for groups and for projects. Valid levels are defined as constants: * ``gitlab.NOTIFICATION_LEVEL_DISABLED`` * ``gitlab.NOTIFICATION_LEVEL_PARTICIPATING`` * ``gitlab.NOTIFICATION_LEVEL_WATCH`` * ``gitlab.NOTIFICATION_LEVEL_GLOBAL`` * ``gitlab.NOTIFICATION_LEVEL_MENTION`` * ``gitlab.NOTIFICATION_LEVEL_CUSTOM`` You get access to fine-grained settings if you use the ``NOTIFICATION_LEVEL_CUSTOM`` level. Reference --------- * v4 API: + :class:`gitlab.v4.objects.NotificationSettings` + :class:`gitlab.v4.objects.NotificationSettingsManager` + :attr:`gitlab.Gitlab.notificationsettings` + :class:`gitlab.v4.objects.GroupNotificationSettings` + :class:`gitlab.v4.objects.GroupNotificationSettingsManager` + :attr:`gitlab.v4.objects.Group.notificationsettings` + :class:`gitlab.v4.objects.ProjectNotificationSettings` + :class:`gitlab.v4.objects.ProjectNotificationSettingsManager` + :attr:`gitlab.v4.objects.Project.notificationsettings` * v3 API: + :class:`gitlab.v3.objects.NotificationSettings` + :class:`gitlab.v3.objects.NotificationSettingsManager` + :attr:`gitlab.Gitlab.notificationsettings` + :class:`gitlab.v3.objects.GroupNotificationSettings` + :class:`gitlab.v3.objects.GroupNotificationSettingsManager` + :attr:`gitlab.v3.objects.Group.notificationsettings` + :class:`gitlab.v3.objects.ProjectNotificationSettings` + :class:`gitlab.v3.objects.ProjectNotificationSettingsManager` + :attr:`gitlab.v3.objects.Project.notificationsettings` * GitLab API: https://docs.gitlab.com/ce/api/notification_settings.html Examples -------- Get the settings: .. literalinclude:: notifications.py :start-after: # get :end-before: # end get Update the settings: .. literalinclude:: notifications.py :start-after: # update :end-before: # end update python-gitlab-1.3.0/docs/gl_objects/pagesdomains.rst000066400000000000000000000023301324224150200225340ustar00rootroot00000000000000############# 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') python-gitlab-1.3.0/docs/gl_objects/projects.py000066400000000000000000000211511324224150200215350ustar00rootroot00000000000000# list # Active projects projects = gl.projects.list() # Archived projects projects = gl.projects.list(archived=1) # Limit to projects with a defined visibility projects = gl.projects.list(visibility='public') # List owned projects projects = gl.projects.owned() # List starred projects projects = gl.projects.starred() # List all the projects projects = gl.projects.all() # Search projects projects = gl.projects.list(search='keyword') # end list # get # Get a project by ID project = gl.projects.get(10) # Get a project by userspace/name project = gl.projects.get('myteam/myproject') # end get # create project = gl.projects.create({'name': 'project1'}) # end create # 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 project.snippets_enabled = 1 project.save() # end update # delete gl.projects.delete(1) # or project.delete() # end delete # fork fork = project.forks.create({}) # fork to a specific namespace fork = project.forks.create({'namespace': 'myteam'}) # end fork # forkrelation project.create_fork_relation(source_project.id) project.delete_fork_relation() # end forkrelation # star project.star() project.unstar() # end star # archive project.archive() project.unarchive() # end archive # members list members = project.members.list() # end members list # members search members = project.members.list(query='bar') # end members search # members get member = project.members.get(1) # end members get # members add member = project.members.create({'user_id': user.id, 'access_level': gitlab.DEVELOPER_ACCESS}) # end members add # members update member.access_level = gitlab.MASTER_ACCESS member.save() # end members update # members delete project.members.delete(user.id) # or member.delete() # end members delete # share project.share(group.id, gitlab.DEVELOPER_ACCESS) # end share # hook list hooks = project.hooks.list() # end hook list # hook get hook = project.hooks.get(1) # end hook get # hook create hook = gl.project_hooks.create({'url': 'http://my/action/url', 'push_events': 1}, project_id=1) # or hook = project.hooks.create({'url': 'http://my/action/url', 'push_events': 1}) # end hook create # hook update hook.push_events = 0 hook.save() # end hook update # hook delete project.hooks.delete(1) # or hook.delete() # end hook delete # repository tree # list the content of the root directory for the default branch items = project.repository_tree() # list the content of a subdirectory on a specific branch items = project.repository_tree(path='docs', ref='branch1') # end repository tree # repository blob items = project.repository_tree(path='docs', ref='branch1') file_info = p.repository_blob(items[0]['id']) content = base64.b64decode(file_info['content']) size = file_info['size'] # end repository blob # repository raw_blob # find the id for the blob (simple search) id = [d['id'] for d in p.repository_tree() if d['name'] == 'README.rst'][0] # get the content file_content = p.repository_raw_blob(id) # end repository raw_blob # repository compare result = project.repository_compare('master', 'branch1') # get the commits for commit in result['commits']: print(commit) # get the diffs for file_diff in result['diffs']: print(file_diff) # end repository compare # repository archive # get the archive for the default branch tgz = project.repository_archive() # get the archive for a branch/tag/commit tgz = project.repository_archive(sha='4567abc') # end repository archive # repository contributors contributors = project.repository_contributors() # end repository contributors # housekeeping project.housekeeping() # end housekeeping # files get f = project.files.get(file_path='README.rst', ref='master') # get the base64 encoded content print(f.content) # get the decoded content print(f.decode()) # end files get # files create # v4 f = project.files.create({'file_path': 'testfile', 'branch': 'master', 'content': file_content, 'commit_message': 'Create testfile'}) # v3 f = project.files.create({'file_path': 'testfile', 'branch_name': 'master', 'content': file_content, 'commit_message': 'Create testfile'}) # end files create # files update f.content = 'new content' f.save(branch='master', commit_message='Update testfile') # v4 f.save(branch_name='master', commit_message='Update testfile') # v3 # or for binary data # Note: decode() is required with python 3 for data serialization. You can omit # it with python 2 f.content = base64.b64encode(open('image.png').read()).decode() f.save(branch='master', commit_message='Update testfile', encoding='base64') # end files update # files delete f.delete(commit_message='Delete testfile') # end files delete # tags list tags = project.tags.list() # end tags list # tags get tag = project.tags.get('1.0') # end tags get # tags create tag = project.tags.create({'tag_name': '1.0', 'ref': 'master'}) # end tags create # tags delete project.tags.delete('1.0') # or tag.delete() # end tags delete # tags release tag.set_release_description('awesome v1.0 release') # end tags release # snippets list snippets = project.snippets.list() # end snippets list # snippets get snippets = project.snippets.list(snippet_id) # end snippets get # snippets create snippet = project.snippets.create({'title': 'sample 1', 'file_name': 'foo.py', 'code': 'import gitlab', 'visibility_level': gitlab.VISIBILITY_PRIVATE}) # end snippets create # snippets content print(snippet.content()) # end snippets content # snippets update snippet.code = 'import gitlab\nimport whatever' snippet.save # end snippets update # snippets delete project.snippets.delete(snippet_id) # or snippet.delete() # end snippets delete # notes list i_notes = issue.notes.list() mr_notes = mr.notes.list() s_notes = snippet.notes.list() # end notes list # notes get i_note = issue.notes.get(note_id) mr_note = mr.notes.get(note_id) s_note = snippet.notes.get(note_id) # end notes get # notes create i_note = issue.notes.create({'body': 'note content'}) mr_note = mr.notes.create({'body': 'note content'}) s_note = snippet.notes.create({'body': 'note content'}) # end notes create # notes update note.body = 'updated note content' note.save() # end notes update # notes delete note.delete() # end notes delete # service get # For v3 service = project.services.get(service_name='asana', project_id=1) # For v4 service = project.services.get('asana') # display its status (enabled/disabled) print(service.active) # end service get # service list services = gl.project_services.available() # end service list # service update service.api_key = 'randomkey' service.save() # end service update # service delete service.delete() # end service delete # boards list boards = project.boards.list() # end boards list # boards get board = project.boards.get(board_id) # end boards get # board lists list b_lists = board.lists.list() # end board lists list # board lists get b_list = board.lists.get(list_id) # end board lists get # board lists create # First get a ProjectLabel label = get_or_create_label() # Then use its ID to create the new board list b_list = board.lists.create({'label_id': label.id}) # end board lists create # board lists update b_list.position = 2 b_list.save() # end board lists update # board lists delete b_list.delete() # end board lists delete # project file upload by path # Or provide a full path to the uploaded file project.upload("filename.txt", filepath="/some/path/filename.txt") # end project file upload by path # project file upload with data # Upload a file using its filename and filedata project.upload("filename.txt", filedata="Raw data") # end project file upload with data # project file upload markdown uploaded_file = project.upload("filename.txt", filedata="data") issue = project.issues.get(issue_id) issue.notes.create({ "body": "See the attached file: {}".format(uploaded_file["markdown"]) }) # end project file upload markdown # project file upload markdown custom uploaded_file = project.upload("filename.txt", filedata="data") issue = project.issues.get(issue_id) issue.notes.create({ "body": "See the [attached file]({})".format(uploaded_file["url"]) }) # end project file upload markdown custom # users list users = p.users.list() # search for users users = p.users.list(search='pattern') # end users list python-gitlab-1.3.0/docs/gl_objects/projects.rst000066400000000000000000000432421324224150200217220ustar00rootroot00000000000000######## Projects ######## Projects ======== Reference --------- * v4 API: + :class:`gitlab.v4.objects.Project` + :class:`gitlab.v4.objects.ProjectManager` + :attr:`gitlab.Gitlab.projects` * v3 API: + :class:`gitlab.v3.objects.Project` + :class:`gitlab.v3.objects.ProjectManager` + :attr:`gitlab.Gitlab.projects` * GitLab API: https://docs.gitlab.com/ce/api/projects.html Examples -------- List projects: The API provides several filtering parameters for the listing methods: * ``archived``: if ``True`` only archived projects will be returned * ``visibility``: returns only projects with the specified visibility (can be ``public``, ``internal`` or ``private``) * ``search``: returns project matching the given pattern Results can also be sorted using the following parameters: * ``order_by``: sort using the given argument. Valid values are ``id``, ``name``, ``path``, ``created_at``, ``updated_at`` and ``last_activity_at``. The default is to sort by ``created_at`` * ``sort``: sort order (``asc`` or ``desc``) .. literalinclude:: projects.py :start-after: # list :end-before: # end list Get a single project: .. literalinclude:: projects.py :start-after: # get :end-before: # end get Create a project: .. literalinclude:: projects.py :start-after: # create :end-before: # end create Create a project for a user (admin only): .. literalinclude:: projects.py :start-after: # user create :end-before: # end user create Create a project in a group: You need to get the id of the group, then use the namespace_id attribute to create the group: .. code:: python group_id = gl.groups.search('my-group')[0].id project = gl.projects.create({'name': 'myrepo', 'namespace_id': group_id}) Update a project: .. literalinclude:: projects.py :start-after: # update :end-before: # end update Delete a project: .. literalinclude:: projects.py :start-after: # delete :end-before: # end delete Fork a project: .. literalinclude:: projects.py :start-after: # fork :end-before: # end fork Create/delete a fork relation between projects (requires admin permissions): .. literalinclude:: projects.py :start-after: # forkrelation :end-before: # end forkrelation Star/unstar a project: .. literalinclude:: projects.py :start-after: # star :end-before: # end star Archive/unarchive a project: .. literalinclude:: projects.py :start-after: # archive :end-before: # end archive .. note:: Previous versions used ``archive_`` and ``unarchive_`` due to a naming issue, they have been deprecated but not yet removed. Start the housekeeping job: .. literalinclude:: projects.py :start-after: # housekeeping :end-before: # end housekeeping List the repository tree: .. literalinclude:: projects.py :start-after: # repository tree :end-before: # end repository tree Get the content and metadata of a file for a commit, using a blob sha: .. literalinclude:: projects.py :start-after: # repository blob :end-before: # end repository blob Get the repository archive: .. literalinclude:: projects.py :start-after: # repository archive :end-before: # end repository archive .. warning:: Archives are entirely stored in memory unless you use the streaming feature. See :ref:`the artifacts example `. Get the content of a file using the blob id: .. literalinclude:: projects.py :start-after: # repository raw_blob :end-before: # end repository raw_blob .. warning:: Blobs are entirely stored in memory unless you use the streaming feature. See :ref:`the artifacts example `. Compare two branches, tags or commits: .. literalinclude:: projects.py :start-after: # repository compare :end-before: # end repository compare Get a list of contributors for the repository: .. literalinclude:: projects.py :start-after: # repository contributors :end-before: # end repository contributors Get a list of users for the repository: .. literalinclude:: projects.py :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 ============= Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectFile` + :class:`gitlab.v4.objects.ProjectFileManager` + :attr:`gitlab.v4.objects.Project.files` * v3 API: + :class:`gitlab.v3.objects.ProjectFile` + :class:`gitlab.v3.objects.ProjectFileManager` + :attr:`gitlab.v3.objects.Project.files` + :attr:`gitlab.Gitlab.project_files` * GitLab API: https://docs.gitlab.com/ce/api/repository_files.html Examples -------- Get a file: .. literalinclude:: projects.py :start-after: # files get :end-before: # end files get Create a new file: .. literalinclude:: projects.py :start-after: # files create :end-before: # end files create Update a file. The entire content must be uploaded, as plain text or as base64 encoded text: .. literalinclude:: projects.py :start-after: # files update :end-before: # end files update Delete a file: .. literalinclude:: projects.py :start-after: # files delete :end-before: # end files delete Project tags ============ Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectTag` + :class:`gitlab.v4.objects.ProjectTagManager` + :attr:`gitlab.v4.objects.Project.tags` * v3 API: + :class:`gitlab.v3.objects.ProjectTag` + :class:`gitlab.v3.objects.ProjectTagManager` + :attr:`gitlab.v3.objects.Project.tags` + :attr:`gitlab.Gitlab.project_tags` * GitLab API: https://docs.gitlab.com/ce/api/tags.html Examples -------- List the project tags: .. literalinclude:: projects.py :start-after: # tags list :end-before: # end tags list Get a tag: .. literalinclude:: projects.py :start-after: # tags get :end-before: # end tags get Create a tag: .. literalinclude:: projects.py :start-after: # tags create :end-before: # end tags create Set or update the release note for a tag: .. literalinclude:: projects.py :start-after: # tags release :end-before: # end tags release Delete a tag: .. literalinclude:: projects.py :start-after: # tags delete :end-before: # end tags delete .. _project_snippets: Project snippets ================ The snippet visibility can be definied using the following constants: * ``gitlab.VISIBILITY_PRIVATE`` * ``gitlab.VISIBILITY_INTERNAL`` * ``gitlab.VISIBILITY_PUBLIC`` Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectSnippet` + :class:`gitlab.v4.objects.ProjectSnippetManager` + :attr:`gitlab.v4.objects.Project.files` * v3 API: + :class:`gitlab.v3.objects.ProjectSnippet` + :class:`gitlab.v3.objects.ProjectSnippetManager` + :attr:`gitlab.v3.objects.Project.files` + :attr:`gitlab.Gitlab.project_files` * GitLab API: https://docs.gitlab.com/ce/api/project_snippets.html Examples -------- List the project snippets: .. literalinclude:: projects.py :start-after: # snippets list :end-before: # end snippets list Get a snippet: .. literalinclude:: projects.py :start-after: # snippets get :end-before: # end snippets get Get the content of a snippet: .. literalinclude:: projects.py :start-after: # snippets content :end-before: # end snippets content .. warning:: The snippet content is entirely stored in memory unless you use the streaming feature. See :ref:`the artifacts example `. Create a snippet: .. literalinclude:: projects.py :start-after: # snippets create :end-before: # end snippets create Update a snippet: .. literalinclude:: projects.py :start-after: # snippets update :end-before: # end snippets update Delete a snippet: .. literalinclude:: projects.py :start-after: # snippets delete :end-before: # end snippets delete Notes ===== You can manipulate notes (comments) on the issues, merge requests and snippets. * :class:`~gitlab.objects.ProjectIssue` with :class:`~gitlab.objects.ProjectIssueNote` * :class:`~gitlab.objects.ProjectMergeRequest` with :class:`~gitlab.objects.ProjectMergeRequestNote` * :class:`~gitlab.objects.ProjectSnippet` with :class:`~gitlab.objects.ProjectSnippetNote` Reference --------- * v4 API: Issues: + :class:`gitlab.v4.objects.ProjectIssueNote` + :class:`gitlab.v4.objects.ProjectIssueNoteManager` + :attr:`gitlab.v4.objects.ProjectIssue.notes` MergeRequests: + :class:`gitlab.v4.objects.ProjectMergeRequestNote` + :class:`gitlab.v4.objects.ProjectMergeRequestNoteManager` + :attr:`gitlab.v4.objects.ProjectMergeRequest.notes` Snippets: + :class:`gitlab.v4.objects.ProjectSnippetNote` + :class:`gitlab.v4.objects.ProjectSnippetNoteManager` + :attr:`gitlab.v4.objects.ProjectSnippet.notes` * v3 API: Issues: + :class:`gitlab.v3.objects.ProjectIssueNote` + :class:`gitlab.v3.objects.ProjectIssueNoteManager` + :attr:`gitlab.v3.objects.ProjectIssue.notes` + :attr:`gitlab.v3.objects.Project.issue_notes` + :attr:`gitlab.Gitlab.project_issue_notes` MergeRequests: + :class:`gitlab.v3.objects.ProjectMergeRequestNote` + :class:`gitlab.v3.objects.ProjectMergeRequestNoteManager` + :attr:`gitlab.v3.objects.ProjectMergeRequest.notes` + :attr:`gitlab.v3.objects.Project.mergerequest_notes` + :attr:`gitlab.Gitlab.project_mergerequest_notes` Snippets: + :class:`gitlab.v3.objects.ProjectSnippetNote` + :class:`gitlab.v3.objects.ProjectSnippetNoteManager` + :attr:`gitlab.v3.objects.ProjectSnippet.notes` + :attr:`gitlab.v3.objects.Project.snippet_notes` + :attr:`gitlab.Gitlab.project_snippet_notes` * GitLab API: https://docs.gitlab.com/ce/api/repository_files.html Examples -------- List the notes for a resource: .. literalinclude:: projects.py :start-after: # notes list :end-before: # end notes list Get a note for a resource: .. literalinclude:: projects.py :start-after: # notes get :end-before: # end notes get Create a note for a resource: .. literalinclude:: projects.py :start-after: # notes create :end-before: # end notes create Update a note for a resource: .. literalinclude:: projects.py :start-after: # notes update :end-before: # end notes update Delete a note for a resource: .. literalinclude:: projects.py :start-after: # notes delete :end-before: # end notes delete Project members =============== Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectMember` + :class:`gitlab.v4.objects.ProjectMemberManager` + :attr:`gitlab.v4.objects.Project.members` * v3 API: + :class:`gitlab.v3.objects.ProjectMember` + :class:`gitlab.v3.objects.ProjectMemberManager` + :attr:`gitlab.v3.objects.Project.members` + :attr:`gitlab.Gitlab.project_members` * GitLab API: https://docs.gitlab.com/ce/api/members.html Examples -------- List the project members: .. literalinclude:: projects.py :start-after: # members list :end-before: # end members list Search project members matching a query string: .. literalinclude:: projects.py :start-after: # members search :end-before: # end members search Get a single project member: .. literalinclude:: projects.py :start-after: # members get :end-before: # end members get Add a project member: .. literalinclude:: projects.py :start-after: # members add :end-before: # end members add Modify a project member (change the access level): .. literalinclude:: projects.py :start-after: # members update :end-before: # end members update Remove a member from the project team: .. literalinclude:: projects.py :start-after: # members delete :end-before: # end members delete Share the project with a group: .. literalinclude:: projects.py :start-after: # share :end-before: # end share Project hooks ============= Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectHook` + :class:`gitlab.v4.objects.ProjectHookManager` + :attr:`gitlab.v4.objects.Project.hooks` * v3 API: + :class:`gitlab.v3.objects.ProjectHook` + :class:`gitlab.v3.objects.ProjectHookManager` + :attr:`gitlab.v3.objects.Project.hooks` + :attr:`gitlab.Gitlab.project_hooks` * GitLab API: https://docs.gitlab.com/ce/api/projects.html#hooks Examples -------- List the project hooks: .. literalinclude:: projects.py :start-after: # hook list :end-before: # end hook list Get a project hook: .. literalinclude:: projects.py :start-after: # hook get :end-before: # end hook get Create a project hook: .. literalinclude:: projects.py :start-after: # hook create :end-before: # end hook create Update a project hook: .. literalinclude:: projects.py :start-after: # hook update :end-before: # end hook update Delete a project hook: .. literalinclude:: projects.py :start-after: # hook delete :end-before: # end hook delete Project Services ================ Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectService` + :class:`gitlab.v4.objects.ProjectServiceManager` + :attr:`gitlab.v4.objects.Project.services` * v3 API: + :class:`gitlab.v3.objects.ProjectService` + :class:`gitlab.v3.objects.ProjectServiceManager` + :attr:`gitlab.v3.objects.Project.services` + :attr:`gitlab.Gitlab.project_services` * GitLab API: https://docs.gitlab.com/ce/api/services.html Exammples --------- Get a service: .. literalinclude:: projects.py :start-after: # service get :end-before: # end service get List the code names of available services (doesn't return objects): .. literalinclude:: projects.py :start-after: # service list :end-before: # end service list Configure and enable a service: .. literalinclude:: projects.py :start-after: # service update :end-before: # end service update Disable a service: .. literalinclude:: projects.py :start-after: # service delete :end-before: # end service delete Issue boards ============ Boards are a visual representation of existing issues for a project. Issues can be moved from one list to the other to track progress and help with priorities. Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectBoard` + :class:`gitlab.v4.objects.ProjectBoardManager` + :attr:`gitlab.v4.objects.Project.boards` * v3 API: + :class:`gitlab.v3.objects.ProjectBoard` + :class:`gitlab.v3.objects.ProjectBoardManager` + :attr:`gitlab.v3.objects.Project.boards` + :attr:`gitlab.Gitlab.project_boards` * GitLab API: https://docs.gitlab.com/ce/api/boards.html Examples -------- Get the list of existing boards for a project: .. literalinclude:: projects.py :start-after: # boards list :end-before: # end boards list Get a single board for a project: .. literalinclude:: projects.py :start-after: # boards get :end-before: # end boards get Board lists =========== Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectBoardList` + :class:`gitlab.v4.objects.ProjectBoardListManager` + :attr:`gitlab.v4.objects.Project.board_lists` * v3 API: + :class:`gitlab.v3.objects.ProjectBoardList` + :class:`gitlab.v3.objects.ProjectBoardListManager` + :attr:`gitlab.v3.objects.ProjectBoard.lists` + :attr:`gitlab.v3.objects.Project.board_lists` + :attr:`gitlab.Gitlab.project_board_lists` * GitLab API: https://docs.gitlab.com/ce/api/boards.html Examples -------- List the issue lists for a board: .. literalinclude:: projects.py :start-after: # board lists list :end-before: # end board lists list Get a single list: .. literalinclude:: projects.py :start-after: # board lists get :end-before: # end board lists get Create a new list: .. literalinclude:: projects.py :start-after: # board lists create :end-before: # end board lists create Change a list position. The first list is at position 0. Moving a list will set it at the given position and move the following lists up a position: .. literalinclude:: projects.py :start-after: # board lists update :end-before: # end board lists update Delete a list: .. literalinclude:: projects.py :start-after: # board lists delete :end-before: # end board lists delete File uploads ============ Reference --------- * v4 API: + :attr:`gitlab.v4.objects.Project.upload` * v3 API: + :attr:`gitlab.v3.objects.Project.upload` * Gitlab API: https://docs.gitlab.com/ce/api/projects.html#upload-a-file Examples -------- Upload a file into a project using a filesystem path: .. literalinclude:: projects.py :start-after: # project file upload by path :end-before: # end project file upload by path Upload a file into a project without a filesystem path: .. literalinclude:: projects.py :start-after: # project file upload with data :end-before: # end project file upload with data Upload a file and comment on an issue using the uploaded file's markdown: .. literalinclude:: projects.py :start-after: # project file upload markdown :end-before: # end project file upload markdown Upload a file and comment on an issue while using custom markdown to reference the uploaded file: .. literalinclude:: projects.py :start-after: # project file upload markdown custom :end-before: # end project file upload markdown custom python-gitlab-1.3.0/docs/gl_objects/protected_branches.rst000066400000000000000000000020031324224150200237150ustar00rootroot00000000000000################## Protected branches ################## You can define a list of protected branch names on a repository. Names can use wildcards (``*``). References ---------- * v4 API: + :class:`gitlab.v4.objects.ProjectProtectedBranch` + :class:`gitlab.v4.objects.ProjectProtectedBranchManager` + :attr:`gitlab.v4.objects.Project.protectedbranches` * GitLab API: https://docs.gitlab.com/ce/api/protected_branches.html#protected-branches-api Examples -------- Get the list of protected branches for a project: .. literalinclude:: branches.py :start-after: # p_branch list :end-before: # end p_branch list Get a single protected branch: .. literalinclude:: branches.py :start-after: # p_branch get :end-before: # end p_branch get Create a protected branch: .. literalinclude:: branches.py :start-after: # p_branch create :end-before: # end p_branch create Delete a protected branch: .. literalinclude:: branches.py :start-after: # p_branch delete :end-before: # end p_branch delete python-gitlab-1.3.0/docs/gl_objects/runners.py000066400000000000000000000012421324224150200213770ustar00rootroot00000000000000# 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 # get runner = gl.runners.get(runner_id) # end get # update runner = gl.runners.get(runner_id) runner.tag_list.append('new_tag') runner.save() # end update # delete gl.runners.delete(runner_id) # or runner.delete() # end delete # project list runners = project.runners.list() # end project list # project enable p_runner = project.runners.create({'runner_id': runner.id}) # end project enable # project disable project.runners.delete(runner.id) # end project disable python-gitlab-1.3.0/docs/gl_objects/runners.rst000066400000000000000000000045031324224150200215620ustar00rootroot00000000000000####### Runners ####### Runners are external processes used to run CI jobs. They are deployed by the administrator and registered to the GitLab instance. Shared runners are available for all projects. Specific runners are enabled for a list of projects. Global runners (admin) ====================== Reference --------- * v4 API: + :class:`gitlab.v4.objects.Runner` + :class:`gitlab.v4.objects.RunnerManager` + :attr:`gitlab.Gitlab.runners` * v3 API: + :class:`gitlab.v3.objects.Runner` + :class:`gitlab.v3.objects.RunnerManager` + :attr:`gitlab.Gitlab.runners` * GitLab API: https://docs.gitlab.com/ce/api/runners.html Examples -------- Use the ``list()`` and ``all()`` methods to list runners. 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:: The returned objects hold minimal information about the runners. Use the ``get()`` method to retrieve detail about a runner. .. literalinclude:: runners.py :start-after: # list :end-before: # end list Get a runner's detail: .. literalinclude:: runners.py :start-after: # get :end-before: # end get Update a runner: .. literalinclude:: runners.py :start-after: # update :end-before: # end update Remove a runner: .. literalinclude:: runners.py :start-after: # delete :end-before: # end delete Project runners =============== Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectRunner` + :class:`gitlab.v4.objects.ProjectRunnerManager` + :attr:`gitlab.v4.objects.Project.runners` * v3 API: + :class:`gitlab.v3.objects.ProjectRunner` + :class:`gitlab.v3.objects.ProjectRunnerManager` + :attr:`gitlab.v3.objects.Project.runners` + :attr:`gitlab.Gitlab.project_runners` * GitLab API: https://docs.gitlab.com/ce/api/runners.html 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 python-gitlab-1.3.0/docs/gl_objects/settings.py000066400000000000000000000001461324224150200215450ustar00rootroot00000000000000# get settings = gl.settings.get() # end get # update s.signin_enabled = False s.save() # end update python-gitlab-1.3.0/docs/gl_objects/settings.rst000066400000000000000000000012121324224150200217200ustar00rootroot00000000000000######## Settings ######## Reference --------- * v4 API: + :class:`gitlab.v4.objects.ApplicationSettings` + :class:`gitlab.v4.objects.ApplicationSettingsManager` + :attr:`gitlab.Gitlab.settings` * v3 API: + :class:`gitlab.v3.objects.ApplicationSettings` + :class:`gitlab.v3.objects.ApplicationSettingsManager` + :attr:`gitlab.Gitlab.settings` * GitLab API: https://docs.gitlab.com/ce/api/settings.html Examples -------- Get the settings: .. literalinclude:: settings.py :start-after: # get :end-before: # end get Update the settings: .. literalinclude:: settings.py :start-after: # update :end-before: # end update python-gitlab-1.3.0/docs/gl_objects/sidekiq.rst000066400000000000000000000007451324224150200215230ustar00rootroot00000000000000############### Sidekiq metrics ############### Reference --------- * v4 API: + :class:`gitlab.v4.objects.SidekiqManager` + :attr:`gitlab.Gitlab.sidekiq` * v3 API: + :class:`gitlab.v3.objects.SidekiqManager` + :attr:`gitlab.Gitlab.sidekiq` * GitLab API: https://docs.gitlab.com/ce/api/sidekiq_metrics.html Examples -------- .. code-block:: python gl.sidekiq.queue_metrics() gl.sidekiq.process_metrics() gl.sidekiq.job_stats() gl.sidekiq.compound_metrics() python-gitlab-1.3.0/docs/gl_objects/snippets.py000066400000000000000000000011211324224150200215440ustar00rootroot00000000000000# list snippets = gl.snippets.list() # end list # public list public_snippets = gl.snippets.public() # end public list # get snippet = gl.snippets.get(snippet_id) # get the content content = snippet.raw() # end get # create snippet = gl.snippets.create({'title': 'snippet1', 'file_name': 'snippet1.py', 'content': open('snippet1.py').read()}) # end create # update snippet.visibility_level = gitlab.Project.VISIBILITY_PUBLIC snippet.save() # end update # delete gl.snippets.delete(snippet_id) # or snippet.delete() # end delete python-gitlab-1.3.0/docs/gl_objects/snippets.rst000066400000000000000000000021141324224150200217270ustar00rootroot00000000000000######## Snippets ######## You can store code snippets in Gitlab. Snippets can be attached to projects (see :ref:`project_snippets`), but can also be detached. * Object class: :class:`gitlab.objects.Namespace` * Manager object: :attr:`gitlab.Gitlab.snippets` Examples ======== List snippets woned by the current user: .. literalinclude:: snippets.py :start-after: # list :end-before: # end list List the public snippets: .. literalinclude:: snippets.py :start-after: # public list :end-before: # end public list Get a snippet: .. literalinclude:: snippets.py :start-after: # get :end-before: # end get .. warning:: Blobs are entirely stored in memory unless you use the streaming feature. See :ref:`the artifacts example `. Create a snippet: .. literalinclude:: snippets.py :start-after: # create :end-before: # end create Update a snippet: .. literalinclude:: snippets.py :start-after: # update :end-before: # end update Delete a snippet: .. literalinclude:: snippets.py :start-after: # delete :end-before: # end delete python-gitlab-1.3.0/docs/gl_objects/system_hooks.py000066400000000000000000000003471324224150200224370ustar00rootroot00000000000000# list hooks = gl.hooks.list() # end list # test gl.hooks.get(hook_id) # end test # create hook = gl.hooks.create({'url': 'http://your.target.url'}) # end create # delete gl.hooks.delete(hook_id) # or hook.delete() # end delete python-gitlab-1.3.0/docs/gl_objects/system_hooks.rst000066400000000000000000000016031324224150200226130ustar00rootroot00000000000000############ System hooks ############ Reference --------- * v4 API: + :class:`gitlab.v4.objects.Hook` + :class:`gitlab.v4.objects.HookManager` + :attr:`gitlab.Gitlab.hooks` * v3 API: + :class:`gitlab.v3.objects.Hook` + :class:`gitlab.v3.objects.HookManager` + :attr:`gitlab.Gitlab.hooks` * GitLab API: https://docs.gitlab.com/ce/api/system_hooks.html Examples -------- List the system hooks: .. literalinclude:: system_hooks.py :start-after: # list :end-before: # end list Create a system hook: .. literalinclude:: system_hooks.py :start-after: # create :end-before: # end create Test a system hook. The returned object is not usable (it misses the hook ID): .. literalinclude:: system_hooks.py :start-after: # test :end-before: # end test Delete a system hook: .. literalinclude:: system_hooks.py :start-after: # delete :end-before: # end delete python-gitlab-1.3.0/docs/gl_objects/templates.py000066400000000000000000000013601324224150200217020ustar00rootroot00000000000000# license list licenses = gl.licenses.list() # end license list # license get license = gl.licenses.get('apache-2.0', project='foobar', fullname='John Doe') print(license.content) # end license get # gitignore list gitignores = gl.gitignores.list() # end gitignore list # gitignore get gitignore = gl.gitignores.get('Python') print(gitignore.content) # end gitignore get # gitlabciyml list gitlabciymls = gl.gitlabciymls.list() # end gitlabciyml list # gitlabciyml get gitlabciyml = gl.gitlabciymls.get('Pelican') print(gitlabciyml.content) # end gitlabciyml get # dockerfile list dockerfiles = gl.dockerfiles.list() # end dockerfile list # dockerfile get dockerfile = gl.dockerfiles.get('Python') print(dockerfile.content) # end dockerfile get python-gitlab-1.3.0/docs/gl_objects/templates.rst000066400000000000000000000053631324224150200220710ustar00rootroot00000000000000######### Templates ######### You can request templates for different type of files: * License files * .gitignore files * GitLab CI configuration files * Dockerfiles License templates ================= Reference --------- * v4 API: + :class:`gitlab.v4.objects.License` + :class:`gitlab.v4.objects.LicenseManager` + :attr:`gitlab.Gitlab.licenses` * v3 API: + :class:`gitlab.v3.objects.License` + :class:`gitlab.v3.objects.LicenseManager` + :attr:`gitlab.Gitlab.licenses` * GitLab API: https://docs.gitlab.com/ce/api/templates/licenses.html Examples -------- List known license templates: .. literalinclude:: templates.py :start-after: # license list :end-before: # end license list Generate a license content for a project: .. literalinclude:: templates.py :start-after: # license get :end-before: # end license get .gitignore templates ==================== Reference --------- * v4 API: + :class:`gitlab.v4.objects.Gitignore` + :class:`gitlab.v4.objects.GitignoreManager` + :attr:`gitlab.Gitlab.gitignores` * v3 API: + :class:`gitlab.v3.objects.Gitignore` + :class:`gitlab.v3.objects.GitignoreManager` + :attr:`gitlab.Gitlab.gitignores` * GitLab API: https://docs.gitlab.com/ce/api/templates/gitignores.html Examples -------- List known gitignore templates: .. literalinclude:: templates.py :start-after: # gitignore list :end-before: # end gitignore list Get a gitignore template: .. literalinclude:: templates.py :start-after: # gitignore get :end-before: # end gitignore get GitLab CI templates =================== Reference --------- * v4 API: + :class:`gitlab.v4.objects.Gitlabciyml` + :class:`gitlab.v4.objects.GitlabciymlManager` + :attr:`gitlab.Gitlab.gitlabciymls` * v3 API: + :class:`gitlab.v3.objects.Gitlabciyml` + :class:`gitlab.v3.objects.GitlabciymlManager` + :attr:`gitlab.Gitlab.gitlabciymls` * GitLab API: https://docs.gitlab.com/ce/api/templates/gitlab_ci_ymls.html Examples -------- List known GitLab CI templates: .. literalinclude:: templates.py :start-after: # gitlabciyml list :end-before: # end gitlabciyml list Get a GitLab CI template: .. literalinclude:: templates.py :start-after: # gitlabciyml get :end-before: # end gitlabciyml get Dockerfile templates ==================== Reference --------- * v4 API: + :class:`gitlab.v4.objects.Dockerfile` + :class:`gitlab.v4.objects.DockerfileManager` + :attr:`gitlab.Gitlab.gitlabciymls` * GitLab API: Not documented. Examples -------- List known Dockerfile templates: .. literalinclude:: templates.py :start-after: # dockerfile list :end-before: # end dockerfile list Get a Dockerfile template: .. literalinclude:: templates.py :start-after: # dockerfile get :end-before: # end dockerfile get python-gitlab-1.3.0/docs/gl_objects/todos.py000066400000000000000000000005221324224150200210330ustar00rootroot00000000000000# list todos = gl.todos.list() # end list # filter todos = gl.todos.list(project_id=1) todos = gl.todos.list(state='done', type='Issue') # end filter # get todo = gl.todos.get(todo_id) # end get # delete gl.todos.delete(todo_id) # or todo.delete() # end delete # all_delete nb_of_closed_todos = gl.todos.delete_all() # end all_delete python-gitlab-1.3.0/docs/gl_objects/todos.rst000066400000000000000000000017421324224150200212200ustar00rootroot00000000000000##### Todos ##### Use :class:`~gitlab.objects.Todo` objects to manipulate todos. The :attr:`gitlab.Gitlab.todos` manager object provides helper functions. Examples -------- List active todos: .. literalinclude:: todos.py :start-after: # list :end-before: # end list You can filter the list using the following parameters: * ``action``: can be ``assigned``, ``mentioned``, ``build_failed``, ``marked``, or ``approval_required`` * ``author_id`` * ``project_id`` * ``state``: can be ``pending`` or ``done`` * ``type``: can be ``Issue`` or ``MergeRequest`` For example: .. literalinclude:: todos.py :start-after: # filter :end-before: # end filter Get a single todo: .. literalinclude:: todos.py :start-after: # get :end-before: # end get Mark a todo as done: .. literalinclude:: todos.py :start-after: # delete :end-before: # end delete Mark all the todos as done: .. literalinclude:: todos.py :start-after: # all_delete :end-before: # end all_delete python-gitlab-1.3.0/docs/gl_objects/users.py000066400000000000000000000040461324224150200210510ustar00rootroot00000000000000# list users = gl.users.list() # end list # search users = gl.users.list(search='oo') # end search # get # by ID user = gl.users.get(2) # by username user = gl.users.list(username='root')[0] # end get # create user = gl.users.create({'email': 'john@doe.com', 'password': 's3cur3s3cr3T', 'username': 'jdoe', 'name': 'John Doe'}) # end create # update user.name = 'Real Name' user.save() # end update # delete gl.users.delete(2) user.delete() # end delete # block user.block() user.unblock() # end block # key list keys = user.keys.list() # end key list # key get key = user.keys.get(1) # end key get # key create k = user.keys.create({'title': 'my_key', 'key': open('/home/me/.ssh/id_rsa.pub').read()}) # end key create # key delete user.keys.delete(1) # or key.delete() # end key delete # gpgkey list gpgkeys = user.gpgkeys.list() # end gpgkey list # gpgkey get gpgkey = user.gpgkeys.get(1) # end gpgkey get # gpgkey create # get the key with `gpg --export -a GPG_KEY_ID` k = user.gpgkeys.create({'key': public_key_content}) # end gpgkey create # gpgkey delete user.gpgkeys.delete(1) # or gpgkey.delete() # end gpgkey delete # email list emails = user.emails.list() # end email list # email get email = gl.user_emails.list(1, user_id=1) # or email = user.emails.get(1) # end email get # email create k = user.emails.create({'email': 'foo@bar.com'}) # end email create # email delete user.emails.delete(1) # or email.delete() # end email delete # currentuser get gl.auth() current_user = gl.user # end currentuser get # it list i_t = user.impersonationtokens.list(state='active') i_t = user.impersonationtokens.list(state='inactive') # end it list # it get i_t = user.impersonationtokens.get(i_t_id) # end it get # it create i_t = user.impersonationtokens.create({'name': 'token1', 'scopes': ['api']}) # use the token to create a new gitlab connection user_gl = gitlab.Gitlab(gitlab_url, private_token=i_t.token) # end it create # it delete i_t.delete() # end it delete python-gitlab-1.3.0/docs/gl_objects/users.rst000066400000000000000000000171501324224150200212310ustar00rootroot00000000000000###################### Users and current user ###################### The Gitlab API exposes user-related method that can be manipulated by admins only. The currently logged-in user is also exposed. Users ===== References ---------- * v4 API: + :class:`gitlab.v4.objects.User` + :class:`gitlab.v4.objects.UserManager` + :attr:`gitlab.Gitlab.users` * v3 API: + :class:`gitlab.v3.objects.User` + :class:`gitlab.v3.objects.UserManager` + :attr:`gitlab.Gitlab.users` * GitLab API: https://docs.gitlab.com/ce/api/users.html Examples -------- Get the list of users: .. literalinclude:: users.py :start-after: # list :end-before: # end list Search users whose username match the given string: .. literalinclude:: users.py :start-after: # search :end-before: # end search Get a single user: .. literalinclude:: users.py :start-after: # get :end-before: # end get Create a user: .. literalinclude:: users.py :start-after: # create :end-before: # end create Update a user: .. literalinclude:: users.py :start-after: # update :end-before: # end update Delete a user: .. literalinclude:: users.py :start-after: # delete :end-before: # end delete Block/Unblock a user: .. literalinclude:: users.py :start-after: # block :end-before: # end block User custom attributes ====================== References ---------- * v4 API: + :class:`gitlab.v4.objects.UserCustomAttribute` + :class:`gitlab.v4.objects.UserCustomAttributeManager` + :attr:`gitlab.v4.objects.User.customattributes` * GitLab API: https://docs.gitlab.com/ce/api/custom_attributes.html Examples -------- List custom attributes for a user:: attrs = user.customattributes.list() Get a custom attribute for a user:: attr = user.customattributes.get(attr_key) Set (create or update) a custom attribute for a user:: attr = user.customattributes.set(attr_key, attr_value) Delete a custom attribute for a user:: 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 ========================= References ---------- * v4 API: + :class:`gitlab.v4.objects.UserImpersonationToken` + :class:`gitlab.v4.objects.UserImpersonationTokenManager` + :attr:`gitlab.v4.objects.User.impersonationtokens` * GitLab API: https://docs.gitlab.com/ce/api/users.html#get-all-impersonation-tokens-of-a-user List impersonation tokens for a user: .. literalinclude:: users.py :start-after: # it list :end-before: # end it list Get an impersonation token for a user: .. literalinclude:: users.py :start-after: # it get :end-before: # end it get Create and use an impersonation token for a user: .. literalinclude:: users.py :start-after: # it create :end-before: # end it create Revoke (delete) an impersonation token for a user: .. literalinclude:: users.py :start-after: # it list :end-before: # end it list Current User ============ References ---------- * v4 API: + :class:`gitlab.v4.objects.CurrentUser` + :class:`gitlab.v4.objects.CurrentUserManager` + :attr:`gitlab.Gitlab.user` * v3 API: + :class:`gitlab.v3.objects.CurrentUser` + :class:`gitlab.v3.objects.CurrentUserManager` + :attr:`gitlab.Gitlab.user` * GitLab API: https://docs.gitlab.com/ce/api/users.html Examples -------- Get the current user: .. literalinclude:: users.py :start-after: # currentuser get :end-before: # end currentuser get GPG keys ======== References ---------- You can manipulate GPG keys for the current user and for the other users if you are admin. * v4 API: + :class:`gitlab.v4.objects.CurrentUserGPGKey` + :class:`gitlab.v4.objects.CurrentUserGPGKeyManager` + :attr:`gitlab.v4.objects.CurrentUser.gpgkeys` + :class:`gitlab.v4.objects.UserGPGKey` + :class:`gitlab.v4.objects.UserGPGKeyManager` + :attr:`gitlab.v4.objects.User.gpgkeys` * GitLab API: https://docs.gitlab.com/ce/api/users.html#list-all-gpg-keys Exemples -------- List GPG keys for a user: .. literalinclude:: users.py :start-after: # gpgkey list :end-before: # end gpgkey list Get an GPG gpgkey for a user: .. literalinclude:: users.py :start-after: # gpgkey get :end-before: # end gpgkey get Create an GPG gpgkey for a user: .. literalinclude:: users.py :start-after: # gpgkey create :end-before: # end gpgkey create Delete an GPG gpgkey for a user: .. literalinclude:: users.py :start-after: # gpgkey delete :end-before: # end gpgkey delete SSH keys ======== References ---------- You can manipulate SSH keys for the current user and for the other users if you are admin. * v4 API: + :class:`gitlab.v4.objects.CurrentUserKey` + :class:`gitlab.v4.objects.CurrentUserKeyManager` + :attr:`gitlab.v4.objects.CurrentUser.keys` + :class:`gitlab.v4.objects.UserKey` + :class:`gitlab.v4.objects.UserKeyManager` + :attr:`gitlab.v4.objects.User.keys` * v3 API: + :class:`gitlab.v3.objects.CurrentUserKey` + :class:`gitlab.v3.objects.CurrentUserKeyManager` + :attr:`gitlab.v3.objects.CurrentUser.keys` + :attr:`gitlab.Gitlab.user.keys` + :class:`gitlab.v3.objects.UserKey` + :class:`gitlab.v3.objects.UserKeyManager` + :attr:`gitlab.v3.objects.User.keys` + :attr:`gitlab.Gitlab.user_keys` * GitLab API: https://docs.gitlab.com/ce/api/users.html#list-ssh-keys Exemples -------- List SSH keys for a user: .. literalinclude:: users.py :start-after: # key list :end-before: # end key list Get an SSH key for a user: .. literalinclude:: users.py :start-after: # key get :end-before: # end key get Create an SSH key for a user: .. literalinclude:: users.py :start-after: # key create :end-before: # end key create Delete an SSH key for a user: .. literalinclude:: users.py :start-after: # key delete :end-before: # end key delete Emails ====== References ---------- You can manipulate emails for the current user and for the other users if you are admin. * v4 API: + :class:`gitlab.v4.objects.CurrentUserEmail` + :class:`gitlab.v4.objects.CurrentUserEmailManager` + :attr:`gitlab.v4.objects.CurrentUser.emails` + :class:`gitlab.v4.objects.UserEmail` + :class:`gitlab.v4.objects.UserEmailManager` + :attr:`gitlab.v4.objects.User.emails` * v3 API: + :class:`gitlab.v3.objects.CurrentUserEmail` + :class:`gitlab.v3.objects.CurrentUserEmailManager` + :attr:`gitlab.v3.objects.CurrentUser.emails` + :attr:`gitlab.Gitlab.user.emails` + :class:`gitlab.v3.objects.UserEmail` + :class:`gitlab.v3.objects.UserEmailManager` + :attr:`gitlab.v3.objects.User.emails` + :attr:`gitlab.Gitlab.user_emails` * GitLab API: https://docs.gitlab.com/ce/api/users.html#list-emails Exemples -------- List emails for a user: .. literalinclude:: users.py :start-after: # email list :end-before: # end email list Get an email for a user: .. literalinclude:: users.py :start-after: # email get :end-before: # end email get Create an email for a user: .. literalinclude:: users.py :start-after: # email create :end-before: # end email create Delete an email for a user: .. literalinclude:: users.py :start-after: # email delete :end-before: # end email delete Users activities ================ References ---------- * v4 only * admin only * v4 API: + :class:`gitlab.v4.objects.UserActivities` + :class:`gitlab.v4.objects.UserActivitiesManager` + :attr:`gitlab.Gitlab.user_activities` * GitLab API: https://docs.gitlab.com/ce/api/users.html#get-user-activities-admin-only Examples -------- Get the users activities: .. code-block:: python activities = gl.user_activities.list(all=True, as_list=False) python-gitlab-1.3.0/docs/gl_objects/wikis.py000066400000000000000000000005261324224150200210350ustar00rootroot00000000000000# list pages = project.wikis.list() # end list # get page = project.wikis.get(page_slug) # end get # create page = project.wikis.create({'title': 'Wiki Page 1', 'content': open(a_file).read()}) # end create # update page.content = 'My new content' page.save() # end update # delete page.delete() # end delete python-gitlab-1.3.0/docs/gl_objects/wikis.rst000066400000000000000000000014031324224150200212100ustar00rootroot00000000000000########## Wiki pages ########## References ========== * v4 API: + :class:`gitlab.v4.objects.ProjectWiki` + :class:`gitlab.v4.objects.ProjectWikiManager` + :attr:`gitlab.v4.objects.Project.wikis` Examples -------- Get the list of wiki pages for a project: .. literalinclude:: wikis.py :start-after: # list :end-before: # end list Get a single wiki page: .. literalinclude:: wikis.py :start-after: # get :end-before: # end get Create a wiki page: .. literalinclude:: wikis.py :start-after: # create :end-before: # end create Update a wiki page: .. literalinclude:: wikis.py :start-after: # update :end-before: # end update Delete a wiki page: .. literalinclude:: wikis.py :start-after: # delete :end-before: # end delete python-gitlab-1.3.0/docs/index.rst000066400000000000000000000010501324224150200170540ustar00rootroot00000000000000.. python-gitlab documentation master file, created by sphinx-quickstart on Mon Dec 8 15:17:39 2014. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to python-gitlab's documentation! ========================================= Contents: .. toctree:: :maxdepth: 2 install cli api-usage switching-to-v4 api-objects api/gitlab release_notes changelog Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` python-gitlab-1.3.0/docs/install.rst000066400000000000000000000010701324224150200174150ustar00rootroot00000000000000############ Installation ############ ``python-gitlab`` is compatible with Python 2.7 and 3.4+. Use :command:`pip` to install the latest stable version of ``python-gitlab``: .. code-block:: console $ sudo pip install --upgrade python-gitlab The current development version is available on `github `__. Use :command:`git` and :command:`python setup.py` to install it: .. code-block:: console $ git clone https://github.com/python-gitlab/python-gitlab $ cd python-gitlab $ sudo python setup.py install python-gitlab-1.3.0/docs/make.bat000066400000000000000000000150731324224150200166320ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\python-gitlab.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\python-gitlab.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end python-gitlab-1.3.0/docs/release_notes.rst000066400000000000000000000000421324224150200205750ustar00rootroot00000000000000.. include:: ../RELEASE_NOTES.rst python-gitlab-1.3.0/docs/switching-to-v4.rst000066400000000000000000000062301324224150200207200ustar00rootroot00000000000000.. _switching_to_v4: ########################## Switching to GitLab API v4 ########################## GitLab provides a new API version (v4) since its 9.0 release. ``python-gitlab`` provides support for this new version, but the python API has been modified to solve some problems with the existing one. GitLab will stop supporting the v3 API soon, and you should consider switching to v4 if you use a recent version of GitLab (>= 9.0), or if you use http://gitlab.com. Using the v4 API ================ 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=3) If you use the configuration file, also explicitly define the version: .. code-block:: ini [my_gitlab] ... api_version = 3 Changes between v3 and v4 API ============================= 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 following important changes in the python API: * managers and objects don't inherit from ``GitlabObject`` and ``BaseManager`` anymore. They inherit from :class:`~gitlab.base.RESTManager` and :class:`~gitlab.base.RESTObject`. * You should only use the managers to perform CRUD operations. The following v3 code: .. code-block:: python gl = gitlab.Gitlab(...) p = Project(gl, project_id) Should be replaced with: .. code-block:: python gl = gitlab.Gitlab(...) p = gl.projects.get(project_id) * Listing methods (``manager.list()`` for instance) can now return generators (:class:`~gitlab.base.RESTObjectList`). They handle the calls to the API when needed to fetch new items. By default you will still get lists. To get generators use ``as_list=False``: .. code-block:: python all_projects_g = gl.projects.list(as_list=False) * The "nested" managers (for instance ``gl.project_issues`` or ``gl.group_members``) are not available anymore. Their goal was to provide a direct way to manage nested objects, and to limit the number of needed API calls. To limit the number of API calls, you can now use ``get()`` methods with the ``lazy=True`` parameter. This creates shallow objects that provide usual managers. The following v3 code: .. code-block:: python issues = gl.project_issues.list(project_id=project_id) Should be replaced with: .. code-block:: python issues = gl.projects.get(project_id, lazy=True).issues.list() This will make only one API call, instead of two if ``lazy`` is not used. * The following :class:`~gitlab.Gitlab` methods should not be used anymore for v4: + ``list()`` + ``get()`` + ``create()`` + ``update()`` + ``delete()`` * If you need to perform HTTP requests to the GitLab server (which you shouldn't), you can use the following :class:`~gitlab.Gitlab` methods: + :attr:`~gitlab.Gitlab.http_request` + :attr:`~gitlab.Gitlab.http_get` + :attr:`~gitlab.Gitlab.http_list` + :attr:`~gitlab.Gitlab.http_post` + :attr:`~gitlab.Gitlab.http_put` + :attr:`~gitlab.Gitlab.http_delete` python-gitlab-1.3.0/gitlab/000077500000000000000000000000001324224150200155315ustar00rootroot00000000000000python-gitlab-1.3.0/gitlab/__init__.py000066400000000000000000001047751324224150200176600ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Copyright (C) 2013-2017 Gauvain Pocentek # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . """Wrapper for the GitLab API.""" from __future__ import print_function from __future__ import absolute_import import importlib import inspect import itertools import json import re import warnings import requests import six import gitlab.config from gitlab.const import * # noqa from gitlab.exceptions import * # noqa from gitlab.v3.objects import * # noqa __title__ = 'python-gitlab' __version__ = '1.3.0' __author__ = 'Gauvain Pocentek' __email__ = 'gauvain@pocentek.net' __license__ = 'LGPL3' __copyright__ = 'Copyright 2013-2018 Gauvain Pocentek' warnings.filterwarnings('default', category=DeprecationWarning, module='^gitlab') def _sanitize(value): if isinstance(value, dict): return dict((k, _sanitize(v)) for k, v in six.iteritems(value)) if isinstance(value, six.string_types): return value.replace('/', '%2F') return value class Gitlab(object): """Represents a GitLab server connection. Args: url (str): The URL of the GitLab server. private_token (str): The user private token oauth_token (str): An oauth token email (str): The user email or login. password (str): The user password (associated with email). ssl_verify (bool|str): Whether SSL certificates should be validated. If the value is a string, it is the path to a CA file used for certificate validation. timeout (float): Timeout to use for requests to the GitLab server. http_username (str): Username for HTTP authentication http_password (str): Password for HTTP authentication api_version (str): Gitlab API version to use (3 or 4) """ 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='4', session=None): self._api_version = str(api_version) self._server_version = self._server_revision = None self._url = '%s/api/v%s' % (url, api_version) #: Timeout to use for requests to gitlab server self.timeout = timeout #: Headers that will be used in request to GitLab self.headers = {} #: The user email self.email = email #: The user password (associated with email) self.password = password #: Whether SSL certificates should be validated self.ssl_verify = ssl_verify self.private_token = private_token self.http_username = http_username self.http_password = http_password self.oauth_token = oauth_token self._set_auth_info() #: Create a session object for requests self.session = session or requests.Session() objects = importlib.import_module('gitlab.v%s.objects' % self._api_version) self._objects = objects self.broadcastmessages = objects.BroadcastMessageManager(self) self.deploykeys = objects.DeployKeyManager(self) self.gitlabciymls = objects.GitlabciymlManager(self) self.gitignores = objects.GitignoreManager(self) self.groups = objects.GroupManager(self) self.hooks = objects.HookManager(self) self.issues = objects.IssueManager(self) self.licenses = objects.LicenseManager(self) self.namespaces = objects.NamespaceManager(self) self.notificationsettings = objects.NotificationSettingsManager(self) self.projects = objects.ProjectManager(self) self.runners = objects.RunnerManager(self) self.settings = objects.ApplicationSettingsManager(self) self.sidekiq = objects.SidekiqManager(self) self.snippets = objects.SnippetManager(self) self.users = objects.UserManager(self) self.todos = objects.TodoManager(self) if self._api_version == '3': 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': # build the "submanagers" for parent_cls in six.itervalues(vars(objects)): if (not inspect.isclass(parent_cls) or not issubclass(parent_cls, objects.GitlabObject) or parent_cls == objects.CurrentUser): continue if not parent_cls.managers: continue for var, cls_name, attrs in parent_cls.managers: prefix = self._cls_to_manager_prefix(parent_cls) var_name = '%s_%s' % (prefix, var) 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') return state def __setstate__(self, state): self.__dict__.update(state) objects = importlib.import_module('gitlab.v%s.objects' % self._api_version) self._objects = objects @property def api_version(self): return self._api_version def _cls_to_manager_prefix(self, cls): # Manage bad naming decisions camel_case = (cls.__name__ .replace('NotificationSettings', 'Notificationsettings') .replace('MergeRequest', 'Mergerequest') .replace('AccessRequest', 'Accessrequest')) return re.sub(r'(.)([A-Z])', r'\1_\2', camel_case).lower() @staticmethod def from_config(gitlab_id=None, config_files=None): """Create a Gitlab connection from configuration files. Args: gitlab_id (str): ID of the configuration section. config_files list[str]: List of paths to configuration files. Returns: (gitlab.Gitlab): A Gitlab connection. Raises: gitlab.config.GitlabDataError: If the configuration is not correct. """ config = gitlab.config.GitlabConfigParser(gitlab_id=gitlab_id, config_files=config_files) return Gitlab(config.url, private_token=config.private_token, oauth_token=config.oauth_token, ssl_verify=config.ssl_verify, timeout=config.timeout, http_username=config.http_username, http_password=config.http_password, api_version=config.api_version) def auth(self): """Performs an authentication. Uses either the private token, or the email/password pair. The `user` attribute will hold a `gitlab.objects.CurrentUser` object on success. """ if self.private_token or self.oauth_token: self._token_auth() else: self._credentials_auth() def _credentials_auth(self): data = {'email': self.email, 'password': self.password} if self.api_version == '3': r = self._raw_post('/session', json.dumps(data), content_type='application/json') raise_error_from_response(r, GitlabAuthenticationError, 201) self.user = self._objects.CurrentUser(self, r.json()) else: r = self.http_post('/session', data) manager = self._objects.CurrentUserManager(self) self.user = self._objects.CurrentUser(manager, r) self.private_token = self.user.private_token self._set_auth_info() def _token_auth(self): if self.api_version == '3': self.user = self._objects.CurrentUser(self) else: self.user = self._objects.CurrentUserManager(self).get() def version(self): """Returns the version and revision of the gitlab server. Note that self.version and self.revision will be set on the gitlab object. Returns: tuple (str, str): The server version and server revision, or ('unknown', 'unknwown') if the server doesn't support this API call (gitlab < 8.13.0) """ if self._server_version is None: r = self._raw_get('/version') try: raise_error_from_response(r, GitlabGetError, 200) data = r.json() self._server_version = data['version'] self._server_revision = data['revision'] except GitlabGetError: self._server_version = self._server_revision = 'unknown' return self._server_version, self._server_revision def _construct_url(self, id_, obj, parameters, action=None): if 'next_url' in parameters: return parameters['next_url'] args = _sanitize(parameters) url_attr = '_url' if action is not None: attr = '_%s_url' % action if hasattr(obj, attr): url_attr = attr obj_url = getattr(obj, url_attr) # TODO(gpocentek): the following will need an update when we have # object with both urlPlural and _ACTION_url attributes if id_ is None and obj._urlPlural is not None: url = obj._urlPlural % args else: url = obj_url % args if id_ is not None: return '%s/%s' % (url, str(id_)) else: return url def _set_auth_info(self): if self.private_token and self.oauth_token: raise ValueError("Only one of private_token or oauth_token should " "be defined") if ((self.http_username and not self.http_password) or (not self.http_username and self.http_password)): raise ValueError("Both http_username and http_password should " "be defined") if self.oauth_token and self.http_username: raise ValueError("Only one of oauth authentication or http " "authentication should be defined") self._http_auth = None if self.private_token: self.headers['PRIVATE-TOKEN'] = self.private_token self.headers.pop('Authorization', None) if self.oauth_token: self.headers['Authorization'] = "Bearer %s" % self.oauth_token self.headers.pop('PRIVATE-TOKEN', None) if self.http_username: self._http_auth = requests.auth.HTTPBasicAuth(self.http_username, self.http_password) def enable_debug(self): import logging try: from http.client import HTTPConnection except ImportError: from httplib import HTTPConnection # noqa HTTPConnection.debuglevel = 1 logging.basicConfig() logging.getLogger().setLevel(logging.DEBUG) requests_log = logging.getLogger("requests.packages.urllib3") requests_log.setLevel(logging.DEBUG) requests_log.propagate = True def _create_headers(self, content_type=None): request_headers = self.headers.copy() if content_type is not None: request_headers['Content-type'] = content_type return request_headers def _get_session_opts(self, content_type): return { 'headers': self._create_headers(content_type), 'auth': self._http_auth, 'timeout': self.timeout, 'verify': self.ssl_verify } def _raw_get(self, path_, content_type=None, streamed=False, **kwargs): if path_.startswith('http://') or path_.startswith('https://'): url = path_ else: url = '%s%s' % (self._url, path_) opts = self._get_session_opts(content_type) try: return self.session.get(url, params=kwargs, stream=streamed, **opts) except Exception as e: raise GitlabConnectionError( "Can't connect to GitLab server (%s)" % e) def _raw_list(self, path_, cls, **kwargs): params = kwargs.copy() catch_recursion_limit = kwargs.get('safe_all', False) get_all_results = (kwargs.get('all', False) is True or catch_recursion_limit) # Remove these keys to avoid breaking the listing (urls will get too # long otherwise) for key in ['all', 'next_url', 'safe_all']: if key in params: del params[key] r = self._raw_get(path_, **params) raise_error_from_response(r, GitlabListError) # These attributes are not needed in the object for key in ['page', 'per_page', 'sudo']: if key in params: del params[key] # Add _from_api manually, because we are not creating objects # through normal path_ params['_from_api'] = True results = [cls(self, item, **params) for item in r.json() if item is not None] try: if ('next' in r.links and 'url' in r.links['next'] and get_all_results): args = kwargs.copy() args['next_url'] = r.links['next']['url'] results.extend(self.list(cls, **args)) except Exception as e: # Catch the recursion limit exception if the 'safe_all' # kwarg was provided if not (catch_recursion_limit and "maximum recursion depth exceeded" in str(e)): raise e return results def _raw_post(self, path_, data=None, content_type=None, files=None, **kwargs): url = '%s%s' % (self._url, path_) opts = self._get_session_opts(content_type) try: return self.session.post(url, params=kwargs, data=data, files=files, **opts) except Exception as e: raise GitlabConnectionError( "Can't connect to GitLab server (%s)" % e) def _raw_put(self, path_, data=None, content_type=None, **kwargs): url = '%s%s' % (self._url, path_) opts = self._get_session_opts(content_type) try: return self.session.put(url, data=data, params=kwargs, **opts) except Exception as e: raise GitlabConnectionError( "Can't connect to GitLab server (%s)" % e) def _raw_delete(self, path_, content_type=None, **kwargs): url = '%s%s' % (self._url, path_) opts = self._get_session_opts(content_type) try: return self.session.delete(url, params=kwargs, **opts) except Exception as e: raise GitlabConnectionError( "Can't connect to GitLab server (%s)" % e) def list(self, obj_class, **kwargs): """Request the listing of GitLab resources. Args: obj_class (object): The class of resource to request. **kwargs: Additional arguments to send to GitLab. Returns: list(obj_class): A list of objects of class `obj_class`. Raises: GitlabConnectionError: If the server cannot be reached. GitlabListError: If the server fails to perform the request. """ missing = [] for k in itertools.chain(obj_class.requiredUrlAttrs, obj_class.requiredListAttrs): if k not in kwargs: missing.append(k) if missing: raise GitlabListError('Missing attribute(s): %s' % ", ".join(missing)) url = self._construct_url(id_=None, obj=obj_class, parameters=kwargs) return self._raw_list(url, obj_class, **kwargs) def get(self, obj_class, id=None, **kwargs): """Request a GitLab resources. Args: obj_class (object): The class of resource to request. id: The object ID. **kwargs: Additional arguments to send to GitLab. Returns: obj_class: An object of class `obj_class`. Raises: GitlabConnectionError: If the server cannot be reached. GitlabGetError: If the server fails to perform the request. """ missing = [] for k in itertools.chain(obj_class.requiredUrlAttrs, obj_class.requiredGetAttrs): if k not in kwargs: missing.append(k) if missing: raise GitlabGetError('Missing attribute(s): %s' % ", ".join(missing)) url = self._construct_url(id_=_sanitize(id), obj=obj_class, parameters=kwargs) r = self._raw_get(url, **kwargs) raise_error_from_response(r, GitlabGetError) return r.json() def delete(self, obj, id=None, **kwargs): """Delete an object on the GitLab server. Args: obj (object or id): The object, or the class of the object to delete. If it is the class, the id of the object must be specified as the `id` arguments. id: ID of the object to remove. Required if `obj` is a class. **kwargs: Additional arguments to send to GitLab. Returns: bool: True if the operation succeeds. Raises: GitlabConnectionError: If the server cannot be reached. GitlabDeleteError: If the server fails to perform the request. """ if inspect.isclass(obj): if not issubclass(obj, GitlabObject): raise GitlabError("Invalid class: %s" % obj) params = {obj.idAttr: id if id else getattr(obj, obj.idAttr)} params.update(kwargs) missing = [] for k in itertools.chain(obj.requiredUrlAttrs, obj.requiredDeleteAttrs): if k not in params: try: params[k] = getattr(obj, k) except KeyError: missing.append(k) if missing: raise GitlabDeleteError('Missing attribute(s): %s' % ", ".join(missing)) obj_id = params[obj.idAttr] if obj._id_in_delete_url else None url = self._construct_url(id_=obj_id, obj=obj, parameters=params) if obj._id_in_delete_url: # The ID is already built, no need to add it as extra key in query # string params.pop(obj.idAttr) r = self._raw_delete(url, **params) raise_error_from_response(r, GitlabDeleteError, expected_code=[200, 202, 204]) return True def create(self, obj, **kwargs): """Create an object on the GitLab server. The object class and attributes define the request to be made on the GitLab server. Args: obj (object): The object to create. **kwargs: Additional arguments to send to GitLab. Returns: str: A json representation of the object as returned by the GitLab server Raises: GitlabConnectionError: If the server cannot be reached. GitlabCreateError: If the server fails to perform the request. """ params = obj.__dict__.copy() params.update(kwargs) missing = [] for k in itertools.chain(obj.requiredUrlAttrs, obj.requiredCreateAttrs): if k not in params: missing.append(k) if missing: raise GitlabCreateError('Missing attribute(s): %s' % ", ".join(missing)) url = self._construct_url(id_=None, obj=obj, parameters=params, action='create') # build data that can really be sent to server data = obj._data_for_gitlab(extra_parameters=kwargs) r = self._raw_post(url, data=data, content_type='application/json') raise_error_from_response(r, GitlabCreateError, 201) return r.json() def update(self, obj, **kwargs): """Update an object on the GitLab server. The object class and attributes define the request to be made on the GitLab server. Args: obj (object): The object to create. **kwargs: Additional arguments to send to GitLab. Returns: str: A json representation of the object as returned by the GitLab server Raises: GitlabConnectionError: If the server cannot be reached. GitlabUpdateError: If the server fails to perform the request. """ params = obj.__dict__.copy() params.update(kwargs) missing = [] if obj.requiredUpdateAttrs or obj.optionalUpdateAttrs: required_attrs = obj.requiredUpdateAttrs else: required_attrs = obj.requiredCreateAttrs for k in itertools.chain(obj.requiredUrlAttrs, required_attrs): if k not in params: missing.append(k) if missing: raise GitlabUpdateError('Missing attribute(s): %s' % ", ".join(missing)) obj_id = params[obj.idAttr] if obj._id_in_update_url else None url = self._construct_url(id_=obj_id, obj=obj, parameters=params) # build data that can really be sent to server data = obj._data_for_gitlab(extra_parameters=kwargs, update=True) r = self._raw_put(url, data=data, content_type='application/json') raise_error_from_response(r, GitlabUpdateError) return r.json() def _build_url(self, path): """Returns the full url from path. If path is already a url, return it unchanged. If it's a path, append it to the stored url. This is a low-level method, different from _construct_url _build_url have no knowledge of GitlabObject's. Returns: str: The full URL """ if path.startswith('http://') or path.startswith('https://'): return path else: return '%s%s' % (self._url, path) def http_request(self, verb, path, query_data={}, post_data={}, streamed=False, files=None, **kwargs): """Make an HTTP request to the Gitlab server. Args: verb (str): The HTTP method to call ('get', 'post', 'put', 'delete') path (str): Path or full URL to query ('/projects' or 'http://whatever/v4/api/projecs') query_data (dict): Data to send as query parameters post_data (dict): Data to send in the body (will be converted to json) streamed (bool): Whether the data should be streamed **kwargs: Extra data to make the query (e.g. sudo, per_page, page) Returns: A requests result object. Raises: GitlabHttpError: When the return code is not 2xx """ def sanitized_url(url): parsed = six.moves.urllib.parse.urlparse(url) new_path = parsed.path.replace('.', '%2E') return parsed._replace(path=new_path).geturl() url = self._build_url(path) 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 if files is not None: del opts["headers"]["Content-type"] verify = opts.pop('verify') timeout = opts.pop('timeout') # Requests assumes that `.` should not be encoded as %2E and will make # changes to urls using this encoding. Using a prepped request we can # get the desired behavior. # The Requests behavior is right but it seems that web servers don't # always agree with this decision (this is the case with a default # gitlab installation) req = requests.Request(verb, url, json=post_data, params=params, files=files, **opts) prepped = self.session.prepare_request(req) prepped.url = sanitized_url(prepped.url) 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 try: error_message = result.json()['message'] except (KeyError, ValueError, TypeError): error_message = result.content if result.status_code == 401: raise GitlabAuthenticationError(response_code=result.status_code, error_message=error_message, response_body=result.content) raise GitlabHttpError(response_code=result.status_code, error_message=error_message, response_body=result.content) def http_get(self, path, query_data={}, streamed=False, **kwargs): """Make a GET request to the Gitlab server. Args: path (str): Path or full URL to query ('/projects' or 'http://whatever/v4/api/projecs') query_data (dict): Data to send as query parameters streamed (bool): Whether the data should be streamed **kwargs: Extra data to make the query (e.g. sudo, per_page, page) Returns: A requests result object is streamed is True or the content type is not json. The parsed json data otherwise. Raises: GitlabHttpError: When the return code is not 2xx GitlabParsingError: If the json data could not be parsed """ result = self.http_request('get', path, query_data=query_data, streamed=streamed, **kwargs) if (result.headers['Content-Type'] == 'application/json' and not streamed): try: return result.json() except Exception: raise GitlabParsingError( error_message="Failed to parse the server message") else: return result def http_list(self, path, query_data={}, as_list=None, **kwargs): """Make a GET request to the Gitlab server for list-oriented queries. Args: path (str): Path or full URL to query ('/projects' or 'http://whatever/v4/api/projecs') query_data (dict): Data to send as query parameters **kwargs: Extra data to make the query (e.g. sudo, per_page, page, all) Returns: list: A list of the objects returned by the server. If `as_list` is False and no pagination-related arguments (`page`, `per_page`, `all`) are defined then a GitlabList object (generator) is returned instead. This object will make API calls when needed to fetch the next items from the server. Raises: GitlabHttpError: When the return code is not 2xx GitlabParsingError: If the json data could not be parsed """ # In case we want to change the default behavior at some point as_list = True if as_list is None else as_list get_all = kwargs.get('all', False) url = self._build_url(path) if get_all is True: return list(GitlabList(self, url, query_data, **kwargs)) 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)) # No pagination, generator requested return GitlabList(self, url, query_data, **kwargs) def http_post(self, path, query_data={}, post_data={}, files=None, **kwargs): """Make a POST request to the Gitlab server. Args: path (str): Path or full URL to query ('/projects' or 'http://whatever/v4/api/projecs') query_data (dict): Data to send as query parameters post_data (dict): Data to send in the body (will be converted to json) **kwargs: Extra data to make the query (e.g. sudo, per_page, page) Returns: The parsed json returned by the server if json is return, else the raw content Raises: GitlabHttpError: When the return code is not 2xx GitlabParsingError: If the json data could not be parsed """ result = self.http_request('post', path, query_data=query_data, post_data=post_data, files=files, **kwargs) try: if result.headers.get('Content-Type', None) == 'application/json': return result.json() except Exception: raise GitlabParsingError( error_message="Failed to parse the server message") return result def http_put(self, path, query_data={}, post_data={}, **kwargs): """Make a PUT request to the Gitlab server. Args: path (str): Path or full URL to query ('/projects' or 'http://whatever/v4/api/projecs') query_data (dict): Data to send as query parameters post_data (dict): Data to send in the body (will be converted to json) **kwargs: Extra data to make the query (e.g. sudo, per_page, page) Returns: The parsed json returned by the server. Raises: GitlabHttpError: When the return code is not 2xx GitlabParsingError: If the json data could not be parsed """ result = self.http_request('put', path, query_data=query_data, post_data=post_data, **kwargs) try: return result.json() except Exception: raise GitlabParsingError( error_message="Failed to parse the server message") def http_delete(self, path, **kwargs): """Make a PUT request to the Gitlab server. Args: path (str): Path or full URL to query ('/projects' or 'http://whatever/v4/api/projecs') **kwargs: Extra data to make the query (e.g. sudo, per_page, page) Returns: The requests object. Raises: GitlabHttpError: When the return code is not 2xx """ return self.http_request('delete', path, **kwargs) class GitlabList(object): """Generator representing a list of remote objects. The object handles the links returned by a query to the API, and will call the API again when needed. """ def __init__(self, gl, url, query_data, get_next=True, **kwargs): self._gl = gl self._query(url, query_data, **kwargs) self._get_next = get_next def _query(self, url, query_data={}, **kwargs): result = self._gl.http_request('get', url, query_data=query_data, **kwargs) try: self._next_url = result.links['next']['url'] except KeyError: self._next_url = None self._current_page = result.headers.get('X-Page') self._prev_page = result.headers.get('X-Prev-Page') self._next_page = result.headers.get('X-Next-Page') self._per_page = result.headers.get('X-Per-Page') self._total_pages = result.headers.get('X-Total-Pages') self._total = result.headers.get('X-Total') try: self._data = result.json() except Exception: raise GitlabParsingError( error_message="Failed to parse the server message") self._current = 0 @property def current_page(self): """The current page number.""" return int(self._current_page) @property def prev_page(self): """The next page number. If None, the current page is the last. """ return int(self._prev_page) if self._prev_page else None @property def next_page(self): """The next page number. If None, the current page is the last. """ return int(self._next_page) if self._next_page else None @property def per_page(self): """The number of items per page.""" return int(self._per_page) @property def total_pages(self): """The total number of pages.""" return int(self._total_pages) @property def total(self): """The total number of items.""" return int(self._total) def __iter__(self): return self def __len__(self): return int(self._total) def __next__(self): return self.next() def next(self): try: item = self._data[self._current] self._current += 1 return item except IndexError: if self._next_url and self._get_next is True: self._query(self._next_url) return self.next() raise StopIteration python-gitlab-1.3.0/gitlab/base.py000066400000000000000000000623661324224150200170320ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Copyright (C) 2013-2017 Gauvain Pocentek # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . import copy import importlib import itertools import json import sys import six import gitlab from gitlab.exceptions import * # noqa class jsonEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, GitlabObject): return obj.as_dict() elif isinstance(obj, gitlab.Gitlab): return {'url': obj._url} return json.JSONEncoder.default(self, obj) class BaseManager(object): """Base manager class for API operations. Managers provide method to manage GitLab API objects, such as retrieval, listing, creation. Inherited class must define the ``obj_cls`` attribute. Attributes: obj_cls (class): class of objects wrapped by this manager. """ obj_cls = None def __init__(self, gl, parent=None, args=[]): """Constructs a manager. Args: gl (gitlab.Gitlab): Gitlab object referencing the GitLab server. parent (Optional[Manager]): A parent manager. args (list): A list of tuples defining a link between the parent/child attributes. Raises: AttributeError: If `obj_cls` is None. """ self.gitlab = gl self.args = args self.parent = parent if self.obj_cls is None: raise AttributeError("obj_cls must be defined") def _set_parent_args(self, **kwargs): args = copy.copy(kwargs) if self.parent is not None: for attr, parent_attr in self.args: args.setdefault(attr, getattr(self.parent, parent_attr)) return args def get(self, id=None, **kwargs): """Get a GitLab object. Args: id: ID of the object to retrieve. **kwargs: Additional arguments to send to GitLab. Returns: object: An object of class `obj_cls`. Raises: NotImplementedError: If objects cannot be retrieved. GitlabGetError: If the server fails to perform the request. """ 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): """Get a list of GitLab objects. Args: **kwargs: Additional arguments to send to GitLab. Returns: list[object]: A list of `obj_cls` objects. Raises: NotImplementedError: If objects cannot be listed. GitlabListError: If the server fails to perform the request. """ args = self._set_parent_args(**kwargs) if not self.obj_cls.canList: raise NotImplementedError return self.obj_cls.list(self.gitlab, **args) def create(self, data, **kwargs): """Create a new object of class `obj_cls`. Args: data (dict): The parameters to send to the GitLab server to create the object. Required and optional arguments are defined in the `requiredCreateAttrs` and `optionalCreateAttrs` of the `obj_cls` class. **kwargs: Additional arguments to send to GitLab. Returns: object: A newly create `obj_cls` object. Raises: NotImplementedError: If objects cannot be created. GitlabCreateError: If the server fails to perform the request. """ args = self._set_parent_args(**kwargs) if not self.obj_cls.canCreate: raise NotImplementedError return self.obj_cls.create(self.gitlab, data, **args) def delete(self, id, **kwargs): """Delete a GitLab object. Args: id: ID of the object to delete. Raises: NotImplementedError: If objects cannot be deleted. GitlabDeleteError: If the server fails to perform the request. """ args = self._set_parent_args(**kwargs) if not self.obj_cls.canDelete: raise NotImplementedError self.gitlab.delete(self.obj_cls, id, **args) class GitlabObject(object): """Base class for all classes that interface with GitLab.""" #: Url to use in GitLab for this object _url = None # Some objects (e.g. merge requests) have different urls for singular and # plural _urlPlural = None _id_in_delete_url = True _id_in_update_url = True _constructorTypes = None #: Tells if GitLab-api allows retrieving single objects. canGet = True #: Tells if GitLab-api allows listing of objects. canList = True #: Tells if GitLab-api allows creation of new objects. canCreate = True #: Tells if GitLab-api allows updating object. canUpdate = True #: Tells if GitLab-api allows deleting object. canDelete = True #: Attributes that are required for constructing url. requiredUrlAttrs = [] #: Attributes that are required when retrieving list of objects. requiredListAttrs = [] #: Attributes that are optional when retrieving list of objects. optionalListAttrs = [] #: Attributes that are optional when retrieving single object. optionalGetAttrs = [] #: Attributes that are required when retrieving single object. requiredGetAttrs = [] #: Attributes that are required when deleting object. requiredDeleteAttrs = [] #: Attributes that are required when creating a new object. requiredCreateAttrs = [] #: Attributes that are optional when creating a new object. optionalCreateAttrs = [] #: Attributes that are required when updating an object. requiredUpdateAttrs = [] #: Attributes that are optional when updating an object. optionalUpdateAttrs = [] #: Whether the object ID is required in the GET url. getRequiresId = True #: List of managers to create. managers = [] #: Name of the identifier of an object. idAttr = 'id' #: Attribute to use as ID when displaying the object. shortPrintAttr = None def _data_for_gitlab(self, extra_parameters={}, update=False, as_json=True): data = {} if update and (self.requiredUpdateAttrs or self.optionalUpdateAttrs): attributes = itertools.chain(self.requiredUpdateAttrs, self.optionalUpdateAttrs) else: attributes = itertools.chain(self.requiredCreateAttrs, self.optionalCreateAttrs) attributes = list(attributes) + ['sudo', 'page', 'per_page'] for attribute in attributes: if hasattr(self, attribute): value = getattr(self, attribute) # 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 data.update(extra_parameters) return json.dumps(data) if as_json else data @classmethod def list(cls, gl, **kwargs): """Retrieve a list of objects from GitLab. Args: gl (gitlab.Gitlab): Gitlab object referencing the GitLab server. per_page (int): Maximum number of items to return. page (int): ID of the page to return when using pagination. Returns: list[object]: A list of objects. Raises: NotImplementedError: If objects can't be listed. GitlabListError: If the server cannot perform the request. """ if not cls.canList: raise NotImplementedError if not cls._url: raise NotImplementedError return gl.list(cls, **kwargs) @classmethod def get(cls, gl, id, **kwargs): """Retrieve a single object. Args: gl (gitlab.Gitlab): Gitlab object referencing the GitLab server. id (int or str): ID of the object to retrieve. Returns: object: The found GitLab object. Raises: NotImplementedError: If objects can't be retrieved. GitlabGetError: If the server cannot perform the request. """ if cls.canGet is False: raise NotImplementedError elif cls.canGet is True: return cls(gl, id, **kwargs) elif cls.canGet == 'from_list': for obj in cls.list(gl, **kwargs): obj_id = getattr(obj, obj.idAttr) if str(obj_id) == str(id): return obj raise GitlabGetError("Object not found") def _get_object(self, k, v, **kwargs): if self._constructorTypes and k in self._constructorTypes: cls = getattr(self._module, self._constructorTypes[k]) return cls(self.gitlab, v, **kwargs) else: return v def _set_from_dict(self, data, **kwargs): if not hasattr(data, 'items'): return for k, v in data.items(): # If a k attribute already exists and is a Manager, do nothing (see # https://github.com/python-gitlab/python-gitlab/issues/209) if isinstance(getattr(self, k, None), BaseManager): continue if isinstance(v, list): self.__dict__[k] = [] for i in v: self.__dict__[k].append(self._get_object(k, i, **kwargs)) elif v is None: self.__dict__[k] = None else: self.__dict__[k] = self._get_object(k, v, **kwargs) def _create(self, **kwargs): if not self.canCreate: raise NotImplementedError json = self.gitlab.create(self, **kwargs) self._set_from_dict(json) self._from_api = True def _update(self, **kwargs): if not self.canUpdate: raise NotImplementedError json = self.gitlab.update(self, **kwargs) self._set_from_dict(json) def save(self, **kwargs): if self._from_api: self._update(**kwargs) else: self._create(**kwargs) def delete(self, **kwargs): if not self.canDelete: raise NotImplementedError if not self._from_api: raise GitlabDeleteError("Object not yet created") return self.gitlab.delete(self, **kwargs) @classmethod def create(cls, gl, data, **kwargs): """Create an object. Args: gl (gitlab.Gitlab): Gitlab object referencing the GitLab server. data (dict): The data used to define the object. Returns: object: The new object. Raises: NotImplementedError: If objects can't be created. GitlabCreateError: If the server cannot perform the request. """ if not cls.canCreate: raise NotImplementedError obj = cls(gl, data, **kwargs) obj.save() return obj def __init__(self, gl, data=None, **kwargs): """Constructs a new object. Do not use this method. Use the `get` or `create` class methods instead. Args: gl (gitlab.Gitlab): Gitlab object referencing the GitLab server. data: If `data` is a dict, create a new object using the information. If it is an int or a string, get a GitLab object from an API request. **kwargs: Additional arguments to send to GitLab. """ self._from_api = False #: (gitlab.Gitlab): Gitlab connection. self.gitlab = gl # store the module in which the object has been created (v3/v4) to be # able to reference other objects from the same module self._module = importlib.import_module(self.__module__) if (data is None or isinstance(data, six.integer_types) or isinstance(data, six.string_types)): if not self.canGet: raise NotImplementedError data = self.gitlab.get(self.__class__, data, **kwargs) self._from_api = True # the API returned a list because custom kwargs where used # instead of the id to request an object. Usually parameters # other than an id return ambiguous results. However in the # gitlab universe iids together with a project_id are # unambiguous for merge requests and issues, too. # So if there is only one element we can use it as our data # source. if 'iid' in kwargs and isinstance(data, list): if len(data) < 1: raise GitlabGetError('Not found') elif len(data) == 1: data = data[0] else: raise GitlabGetError('Impossible! You found multiple' ' elements with the same iid.') self._set_from_dict(data, **kwargs) if kwargs: for k, v in kwargs.items(): # Don't overwrite attributes returned by the server (#171) if k not in self.__dict__ or not self.__dict__[k]: self.__dict__[k] = v # Special handling for api-objects that don't have id-number in api # responses. Currently only Labels and Files if not hasattr(self, "id"): self.id = None def __getstate__(self): state = self.__dict__.copy() module = state.pop('_module') state['_module_name'] = module.__name__ return state def __setstate__(self, state): module_name = state.pop('_module_name') self.__dict__.update(state) self._module = importlib.import_module(module_name) def _set_manager(self, var, cls, attrs): manager = cls(self.gitlab, self, attrs) setattr(self, var, manager) def __getattr__(self, name): # build a manager if it doesn't exist yet for var, cls, attrs in self.managers: if var != name: continue # Build the full class path if needed if isinstance(cls, six.string_types): cls = getattr(self._module, cls) self._set_manager(var, cls, attrs) return getattr(self, var) raise AttributeError(name) def __str__(self): return '%s => %s' % (type(self), str(self.__dict__)) def __repr__(self): return '<%s %s:%s>' % (self.__class__.__name__, self.idAttr, getattr(self, self.idAttr)) def display(self, pretty): if pretty: self.pretty_print() else: self.short_print() def short_print(self, depth=0): """Print the object on the standard output (verbose). Args: depth (int): Used internaly for recursive call. """ id = self.__dict__[self.idAttr] print("%s%s: %s" % (" " * depth * 2, self.idAttr, id)) if self.shortPrintAttr: print("%s%s: %s" % (" " * depth * 2, self.shortPrintAttr.replace('_', '-'), self.__dict__[self.shortPrintAttr])) @staticmethod def _get_display_encoding(): return sys.stdout.encoding or sys.getdefaultencoding() @staticmethod def _obj_to_str(obj): if isinstance(obj, dict): s = ", ".join(["%s: %s" % (x, GitlabObject._obj_to_str(y)) for (x, y) in obj.items()]) return "{ %s }" % s elif isinstance(obj, list): s = ", ".join([GitlabObject._obj_to_str(x) for x in obj]) return "[ %s ]" % s elif six.PY2 and isinstance(obj, six.text_type): return obj.encode(GitlabObject._get_display_encoding(), "replace") else: return str(obj) def pretty_print(self, depth=0): """Print the object on the standard output (verbose). Args: depth (int): Used internaly for recursive call. """ id = self.__dict__[self.idAttr] print("%s%s: %s" % (" " * depth * 2, self.idAttr, id)) for k in sorted(self.__dict__.keys()): if k in (self.idAttr, 'id', 'gitlab'): continue if k[0] == '_': continue v = self.__dict__[k] pretty_k = k.replace('_', '-') if six.PY2: pretty_k = pretty_k.encode( GitlabObject._get_display_encoding(), "replace") if isinstance(v, GitlabObject): if depth == 0: print("%s:" % pretty_k) v.pretty_print(1) else: print("%s: %s" % (pretty_k, v.id)) elif isinstance(v, BaseManager): continue else: if hasattr(v, __name__) and v.__name__ == 'Gitlab': continue v = GitlabObject._obj_to_str(v) print("%s%s: %s" % (" " * depth * 2, pretty_k, v)) def json(self): """Dump the object as json. Returns: str: The json string. """ return json.dumps(self, cls=jsonEncoder) def as_dict(self): """Dump the object as a dict.""" return {k: v for k, v in six.iteritems(self.__dict__) if (not isinstance(v, BaseManager) and not k[0] == '_')} def __eq__(self, other): if type(other) is type(self): return self.as_dict() == other.as_dict() return False def __ne__(self, other): return not self.__eq__(other) class RESTObject(object): """Represents an object built from server data. It holds the attributes know from the server, and the updated attributes in another. This allows smart updates, if the object allows it. You can redefine ``_id_attr`` in child classes to specify which attribute must be used as uniq ID. ``None`` means that the object can be updated without ID in the url. """ _id_attr = 'id' def __init__(self, manager, attrs): self.__dict__.update({ 'manager': manager, '_attrs': attrs, '_updated_attrs': {}, '_module': importlib.import_module(self.__module__) }) self.__dict__['_parent_attrs'] = self.manager.parent_attrs self._create_managers() def __getstate__(self): state = self.__dict__.copy() module = state.pop('_module') state['_module_name'] = module.__name__ return state def __setstate__(self, state): module_name = state.pop('_module_name') self.__dict__.update(state) self._module = importlib.import_module(module_name) def __getattr__(self, name): try: return self.__dict__['_updated_attrs'][name] except KeyError: try: value = self.__dict__['_attrs'][name] # If the value is a list, we copy it in the _updated_attrs dict # because we are not able to detect changes made on the object # (append, insert, pop, ...). Without forcing the attr # creation __setattr__ is never called, the list never ends up # in the _updated_attrs dict, and the update() and save() # method never push the new data to the server. # See https://github.com/python-gitlab/python-gitlab/issues/306 # # note: _parent_attrs will only store simple values (int) so we # don't make this check in the next except block. if isinstance(value, list): self.__dict__['_updated_attrs'][name] = value[:] return self.__dict__['_updated_attrs'][name] return value except KeyError: try: return self.__dict__['_parent_attrs'][name] except KeyError: raise AttributeError(name) def __setattr__(self, name, value): self.__dict__['_updated_attrs'][name] = value def __str__(self): data = self._attrs.copy() data.update(self._updated_attrs) return '%s => %s' % (type(self), data) def __repr__(self): if self._id_attr: return '<%s %s:%s>' % (self.__class__.__name__, self._id_attr, self.get_id()) else: return '<%s>' % self.__class__.__name__ def _create_managers(self): managers = getattr(self, '_managers', None) if managers is None: return for attr, cls_name in self._managers: cls = getattr(self._module, cls_name) manager = cls(self.manager.gitlab, parent=self) self.__dict__[attr] = manager def _update_attrs(self, new_attrs): self.__dict__['_updated_attrs'] = {} self.__dict__['_attrs'].update(new_attrs) def get_id(self): """Returns the id of the resource.""" if self._id_attr is None: return None return getattr(self, self._id_attr) @property def attributes(self): d = self.__dict__['_updated_attrs'].copy() d.update(self.__dict__['_attrs']) d.update(self.__dict__['_parent_attrs']) return d class RESTObjectList(object): """Generator object representing a list of RESTObject's. This generator uses the Gitlab pagination system to fetch new data when required. Note: you should not instanciate such objects, they are returned by calls to RESTManager.list() Args: manager: Manager to attach to the created objects obj_cls: Type of objects to create from the json data _list: A GitlabList object """ def __init__(self, manager, obj_cls, _list): """Creates an objects list from a GitlabList. You should not create objects of this type, but use managers list() methods instead. Args: manager: the RESTManager to attach to the objects obj_cls: the class of the created objects _list: the GitlabList holding the data """ self.manager = manager self._obj_cls = obj_cls self._list = _list def __iter__(self): return self def __len__(self): return len(self._list) def __next__(self): return self.next() def next(self): data = self._list.next() return self._obj_cls(self.manager, data) @property def current_page(self): """The current page number.""" return self._list.current_page @property def prev_page(self): """The next page number. If None, the current page is the last. """ return self._list.prev_page @property def next_page(self): """The next page number. If None, the current page is the last. """ return self._list.next_page @property def per_page(self): """The number of items per page.""" return self._list.per_page @property def total_pages(self): """The total number of pages.""" return self._list.total_pages @property def total(self): """The total number of items.""" return self._list.total class RESTManager(object): """Base class for CRUD operations on objects. Derivated class must define ``_path`` and ``_obj_cls``. ``_path``: Base URL path on which requests will be sent (e.g. '/projects') ``_obj_cls``: The class of objects that will be created """ _path = None _obj_cls = None def __init__(self, gl, parent=None): """REST manager constructor. Args: gl (Gitlab): :class:`~gitlab.Gitlab` connection to use to make requests. parent: REST object to which the manager is attached. """ self.gitlab = gl self._parent = parent # for nested managers self._computed_path = self._compute_path() @property def parent_attrs(self): return self._parent_attrs def _compute_path(self, path=None): self._parent_attrs = {} if path is None: path = self._path if self._parent is None or not hasattr(self, '_from_parent_attrs'): return path 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 @property def path(self): return self._computed_path python-gitlab-1.3.0/gitlab/cli.py000066400000000000000000000121131324224150200166500ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright (C) 2013-2017 Gauvain Pocentek # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . from __future__ import print_function import argparse import functools import importlib import re import sys import gitlab.config camel_re = re.compile('(.)([A-Z])') # custom_actions = { # cls: { # action: (mandatory_args, optional_args, in_obj), # }, # } custom_actions = {} def register_custom_action(cls_names, mandatory=tuple(), optional=tuple()): def wrap(f): @functools.wraps(f) def wrapped_f(*args, **kwargs): return f(*args, **kwargs) # in_obj defines whether the method belongs to the obj or the manager in_obj = True classes = cls_names if type(cls_names) != tuple: classes = (cls_names, ) for cls_name in classes: final_name = cls_name if cls_name.endswith('Manager'): final_name = cls_name.replace('Manager', '') in_obj = False if final_name not in custom_actions: custom_actions[final_name] = {} action = f.__name__.replace('_', '-') custom_actions[final_name][action] = (mandatory, optional, in_obj) return wrapped_f return wrap def die(msg, e=None): if e: msg = "%s (%s)" % (msg, e) sys.stderr.write(msg + "\n") sys.exit(1) def what_to_cls(what): return "".join([s.capitalize() for s in what.split("-")]) def cls_to_what(cls): return camel_re.sub(r'\1-\2', cls.__name__).lower() def _get_base_parser(): parser = argparse.ArgumentParser( description="GitLab API Command Line Interface") parser.add_argument("--version", help="Display the version.", action="store_true") parser.add_argument("-v", "--verbose", "--fancy", help="Verbose mode (legacy format only)", action="store_true") parser.add_argument("-d", "--debug", help="Debug mode (display HTTP requests)", action="store_true") parser.add_argument("-c", "--config-file", action='append', help=("Configuration file to use. Can be used " "multiple times.")) parser.add_argument("-g", "--gitlab", help=("Which configuration section should " "be used. If not defined, the default selection " "will be used."), required=False) parser.add_argument("-o", "--output", help=("Output format (v4 only): json|legacy|yaml"), required=False, choices=['json', 'legacy', 'yaml'], default="legacy") parser.add_argument("-f", "--fields", help=("Fields to display in the output (comma " "separated). Not used with legacy output"), required=False) return parser def _get_parser(cli_module): parser = _get_base_parser() return cli_module.extend_parser(parser) def main(): if "--version" in sys.argv: print(gitlab.__version__) exit(0) parser = _get_base_parser() (options, args) = parser.parse_known_args(sys.argv) config = gitlab.config.GitlabConfigParser(options.gitlab, options.config_file) cli_module = importlib.import_module('gitlab.v%s.cli' % config.api_version) parser = _get_parser(cli_module) args = parser.parse_args(sys.argv[1:]) config_files = args.config_file gitlab_id = args.gitlab verbose = args.verbose output = args.output fields = [] if args.fields: fields = [x.strip() for x in args.fields.split(',')] debug = args.debug action = args.action what = args.what args = args.__dict__ # Remove CLI behavior-related args for item in ('gitlab', 'config_file', 'verbose', 'debug', 'what', 'action', 'version', 'output'): args.pop(item) args = {k: v for k, v in args.items() if v is not None} try: gl = gitlab.Gitlab.from_config(gitlab_id, config_files) if gl.private_token or gl.oauth_token: gl.auth() except Exception as e: die(str(e)) if debug: gl.enable_debug() cli_module.run(gl, what, action, args, verbose, output, fields) sys.exit(0) python-gitlab-1.3.0/gitlab/config.py000066400000000000000000000113751324224150200173570ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Copyright (C) 2013-2017 Gauvain Pocentek # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . import os from six.moves import configparser _DEFAULT_FILES = [ '/etc/python-gitlab.cfg', os.path.expanduser('~/.python-gitlab.cfg') ] class ConfigError(Exception): pass class GitlabIDError(ConfigError): pass class GitlabDataError(ConfigError): pass class GitlabConfigParser(object): def __init__(self, gitlab_id=None, config_files=None): self.gitlab_id = gitlab_id _files = config_files or _DEFAULT_FILES self._config = configparser.ConfigParser() self._config.read(_files) if self.gitlab_id is None: try: self.gitlab_id = self._config.get('global', 'default') except Exception: raise GitlabIDError("Impossible to get the gitlab id " "(not specified in config file)") try: self.url = self._config.get(self.gitlab_id, 'url') except Exception: raise GitlabDataError("Impossible to get gitlab informations from " "configuration (%s)" % self.gitlab_id) self.ssl_verify = True try: self.ssl_verify = self._config.getboolean('global', 'ssl_verify') except ValueError: # Value Error means the option exists but isn't a boolean. # Get as a string instead as it should then be a local path to a # CA bundle. try: self.ssl_verify = self._config.get('global', 'ssl_verify') except Exception: pass except Exception: pass try: self.ssl_verify = self._config.getboolean(self.gitlab_id, 'ssl_verify') except ValueError: # Value Error means the option exists but isn't a boolean. # Get as a string instead as it should then be a local path to a # CA bundle. try: self.ssl_verify = self._config.get(self.gitlab_id, 'ssl_verify') except Exception: pass except Exception: pass self.timeout = 60 try: self.timeout = self._config.getint('global', 'timeout') except Exception: pass try: self.timeout = self._config.getint(self.gitlab_id, 'timeout') except Exception: pass self.private_token = None try: self.private_token = self._config.get(self.gitlab_id, 'private_token') except Exception: pass self.oauth_token = None try: self.oauth_token = self._config.get(self.gitlab_id, 'oauth_token') except Exception: pass self.http_username = None self.http_password = None try: self.http_username = self._config.get(self.gitlab_id, 'http_username') self.http_password = self._config.get(self.gitlab_id, 'http_password') except Exception: pass self.http_username = None self.http_password = None try: self.http_username = self._config.get(self.gitlab_id, 'http_username') self.http_password = self._config.get(self.gitlab_id, 'http_password') except Exception: pass 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: pass if self.api_version not in ('3', '4'): raise GitlabDataError("Unsupported API version: %s" % self.api_version) python-gitlab-1.3.0/gitlab/const.py000066400000000000000000000022111324224150200172250ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Copyright (C) 2016-2017 Gauvain Pocentek # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . GUEST_ACCESS = 10 REPORTER_ACCESS = 20 DEVELOPER_ACCESS = 30 MASTER_ACCESS = 40 OWNER_ACCESS = 50 VISIBILITY_PRIVATE = 0 VISIBILITY_INTERNAL = 10 VISIBILITY_PUBLIC = 20 NOTIFICATION_LEVEL_DISABLED = 'disabled' NOTIFICATION_LEVEL_PARTICIPATING = 'participating' NOTIFICATION_LEVEL_WATCH = 'watch' NOTIFICATION_LEVEL_GLOBAL = 'global' NOTIFICATION_LEVEL_MENTION = 'mention' NOTIFICATION_LEVEL_CUSTOM = 'custom' python-gitlab-1.3.0/gitlab/exceptions.py000066400000000000000000000125761324224150200202770ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Copyright (C) 2013-2017 Gauvain Pocentek # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . import functools class GitlabError(Exception): def __init__(self, error_message="", response_code=None, response_body=None): Exception.__init__(self, error_message) # Http status code self.response_code = response_code # Full http response self.response_body = response_body # Parsed error message from gitlab self.error_message = error_message def __str__(self): if self.response_code is not None: return "{0}: {1}".format(self.response_code, self.error_message) else: return "{0}".format(self.error_message) class GitlabAuthenticationError(GitlabError): pass class GitlabParsingError(GitlabError): pass class GitlabConnectionError(GitlabError): pass class GitlabOperationError(GitlabError): pass class GitlabHttpError(GitlabError): pass class GitlabListError(GitlabOperationError): pass class GitlabGetError(GitlabOperationError): pass class GitlabCreateError(GitlabOperationError): pass class GitlabUpdateError(GitlabOperationError): pass class GitlabDeleteError(GitlabOperationError): pass class GitlabSetError(GitlabOperationError): pass class GitlabProtectError(GitlabOperationError): pass class GitlabTransferProjectError(GitlabOperationError): pass class GitlabProjectDeployKeyError(GitlabOperationError): pass class GitlabCancelError(GitlabOperationError): pass class GitlabPipelineCancelError(GitlabCancelError): pass class GitlabRetryError(GitlabOperationError): pass class GitlabBuildCancelError(GitlabCancelError): pass class GitlabBuildRetryError(GitlabRetryError): pass class GitlabBuildPlayError(GitlabRetryError): pass class GitlabBuildEraseError(GitlabRetryError): pass class GitlabJobCancelError(GitlabCancelError): pass class GitlabJobRetryError(GitlabRetryError): pass class GitlabJobPlayError(GitlabRetryError): pass class GitlabJobEraseError(GitlabRetryError): pass class GitlabPipelineRetryError(GitlabRetryError): pass class GitlabBlockError(GitlabOperationError): pass class GitlabUnblockError(GitlabOperationError): pass class GitlabSubscribeError(GitlabOperationError): pass class GitlabUnsubscribeError(GitlabOperationError): pass class GitlabMRForbiddenError(GitlabOperationError): pass class GitlabMRClosedError(GitlabOperationError): pass class GitlabMROnBuildSuccessError(GitlabOperationError): pass class GitlabTodoError(GitlabOperationError): pass class GitlabTimeTrackingError(GitlabOperationError): pass class GitlabUploadError(GitlabOperationError): pass class GitlabAttachFileError(GitlabOperationError): pass class GitlabCherryPickError(GitlabOperationError): pass 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. Do nothing if the response status is the expected one. If response status code is 401, raises instead GitlabAuthenticationError. Args: response: requests response object error: Error-class or dict {return-code => class} of possible error class to raise. Should be inherited from GitLabError """ if isinstance(expected_code, int): expected_codes = [expected_code] else: expected_codes = expected_code if response.status_code in expected_codes: return try: message = response.json()['message'] except (KeyError, ValueError, TypeError): message = response.content if isinstance(error, dict): error = error.get(response.status_code, GitlabOperationError) else: if response.status_code == 401: error = GitlabAuthenticationError raise error(error_message=message, response_code=response.status_code, response_body=response.content) def on_http_error(error): """Manage GitlabHttpError exceptions. This decorator function can be used to catch GitlabHttpError exceptions raise specialized exceptions instead. Args: error(Exception): The exception type to raise -- must inherit from GitlabError """ def wrap(f): @functools.wraps(f) def wrapped_f(*args, **kwargs): try: return f(*args, **kwargs) except GitlabHttpError as e: raise error(e.error_message, e.response_code, e.response_body) return wrapped_f return wrap python-gitlab-1.3.0/gitlab/mixins.py000066400000000000000000000425121324224150200174160ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Copyright (C) 2013-2017 Gauvain Pocentek # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . import gitlab from gitlab import base from gitlab import cli from gitlab import exceptions as exc class GetMixin(object): @exc.on_http_error(exc.GitlabGetError) def get(self, id, lazy=False, **kwargs): """Retrieve a single object. Args: id (int or str): ID of the object to retrieve lazy (bool): If True, don't request the server, but create a shallow object giving access to the managers. This is useful if you want to avoid useless calls to the API. **kwargs: Extra options to send to the Gitlab server (e.g. sudo) Returns: object: The generated RESTObject. Raises: GitlabAuthenticationError: If authentication is not correct GitlabGetError: If the server cannot perform the request """ if not isinstance(id, int): id = id.replace('/', '%2F') path = '%s/%s' % (self.path, id) if lazy is True: return self._obj_cls(self, {self._obj_cls._id_attr: id}) server_data = self.gitlab.http_get(path, **kwargs) return self._obj_cls(self, server_data) class GetWithoutIdMixin(object): @exc.on_http_error(exc.GitlabGetError) def get(self, id=None, **kwargs): """Retrieve a single object. Args: **kwargs: Extra options to send to the Gitlab server (e.g. sudo) Returns: object: The generated RESTObject Raises: GitlabAuthenticationError: If authentication is not correct GitlabGetError: If the server cannot perform the request """ server_data = self.gitlab.http_get(self.path, **kwargs) return self._obj_cls(self, server_data) class ListMixin(object): @exc.on_http_error(exc.GitlabListError) 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 """ # Allow to overwrite the path, handy for custom listings path = kwargs.pop('path', self.path) obj = self.gitlab.http_list(path, **kwargs) if isinstance(obj, list): return [self._obj_cls(self, item) for item in obj] else: return base.RESTObjectList(self, self._obj_cls, obj) class GetFromListMixin(ListMixin): def get(self, id, **kwargs): """Retrieve a single object. Args: id (int or str): ID of the object to retrieve **kwargs: Extra options to send to the Gitlab server (e.g. sudo) Returns: object: The generated RESTObject Raises: GitlabAuthenticationError: If authentication is not correct GitlabGetError: If the server cannot perform the request """ try: gen = self.list() except exc.GitlabListError: raise exc.GitlabGetError(response_code=404, error_message="Not found") for obj in gen: if str(obj.get_id()) == str(id): return obj raise exc.GitlabGetError(response_code=404, error_message="Not found") class RetrieveMixin(ListMixin, GetMixin): pass class CreateMixin(object): def _check_missing_create_attrs(self, data): required, optional = self.get_create_attrs() missing = [] for attr in required: if attr not in data: missing.append(attr) continue if missing: raise AttributeError("Missing attributes: %s" % ", ".join(missing)) def get_create_attrs(self): """Return the required and optional arguments. Returns: tuple: 2 items: list of required arguments and list of optional arguments for creation (in that order) """ return getattr(self, '_create_attrs', (tuple(), tuple())) @exc.on_http_error(exc.GitlabCreateError) def create(self, data, **kwargs): """Create a new object. Args: data (dict): parameters to send to the server to create the resource **kwargs: Extra options to send to the Gitlab server (e.g. sudo) Returns: RESTObject: a new instance of the managed object class built with the data sent by the server Raises: GitlabAuthenticationError: If authentication is not correct GitlabCreateError: If the server cannot perform the request """ self._check_missing_create_attrs(data) if hasattr(self, '_sanitize_data'): data = self._sanitize_data(data, 'create') # Handle specific URL for creation path = kwargs.pop('path', self.path) server_data = self.gitlab.http_post(path, post_data=data, **kwargs) return self._obj_cls(self, server_data) class UpdateMixin(object): def _check_missing_update_attrs(self, data): required, optional = self.get_update_attrs() missing = [] for attr in required: if attr not in data: missing.append(attr) continue if missing: raise AttributeError("Missing attributes: %s" % ", ".join(missing)) def get_update_attrs(self): """Return the required and optional arguments. Returns: tuple: 2 items: list of required arguments and list of optional arguments for update (in that order) """ return getattr(self, '_update_attrs', (tuple(), tuple())) @exc.on_http_error(exc.GitlabUpdateError) def update(self, id=None, new_data={}, **kwargs): """Update an object on the server. Args: id: ID of the object to update (can be None if not required) new_data: the update data for the object **kwargs: Extra options to send to the Gitlab server (e.g. sudo) Returns: dict: The new object data (*not* a RESTObject) Raises: GitlabAuthenticationError: If authentication is not correct GitlabUpdateError: If the server cannot perform the request """ if id is None: path = self.path else: path = '%s/%s' % (self.path, id) self._check_missing_update_attrs(new_data) if hasattr(self, '_sanitize_data'): data = self._sanitize_data(new_data, 'update') else: data = new_data return self.gitlab.http_put(path, post_data=data, **kwargs) class SetMixin(object): @exc.on_http_error(exc.GitlabSetError) def set(self, key, value, **kwargs): """Create or update the object. Args: key (str): The key of the object to create/update value (str): The value to set for the object **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, key.replace('/', '%2F')) data = {'value': value} server_data = self.gitlab.http_put(path, post_data=data, **kwargs) return self._obj_cls(self, server_data) class DeleteMixin(object): @exc.on_http_error(exc.GitlabDeleteError) def delete(self, id, **kwargs): """Delete an object on the server. Args: id: ID of the object to delete **kwargs: Extra options to send to the Gitlab server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabDeleteError: If the server cannot perform the request """ if not isinstance(id, int): id = id.replace('/', '%2F') path = '%s/%s' % (self.path, id) self.gitlab.http_delete(path, **kwargs) class CRUDMixin(GetMixin, ListMixin, CreateMixin, UpdateMixin, DeleteMixin): pass class NoUpdateMixin(GetMixin, ListMixin, CreateMixin, DeleteMixin): pass class SaveMixin(object): """Mixin for RESTObject's that can be updated.""" def _get_updated_data(self): updated_data = {} required, optional = self.manager.get_update_attrs() for attr in required: # Get everything required, no matter if it's been updated updated_data[attr] = getattr(self, attr) # Add the updated attributes updated_data.update(self._updated_attrs) return updated_data def save(self, **kwargs): """Save the changes made to the object to the server. The object is updated to match what the server returns. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raise: GitlabAuthenticationError: If authentication is not correct 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() server_data = self.manager.update(obj_id, updated_data, **kwargs) if server_data is not None: self._update_attrs(server_data) class ObjectDeleteMixin(object): """Mixin for RESTObject's that can be deleted.""" def delete(self, **kwargs): """Delete the object from the server. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabDeleteError: If the server cannot perform the request """ self.manager.delete(self.get_id()) class AccessRequestMixin(object): @cli.register_custom_action(('ProjectAccessRequest', 'GroupAccessRequest'), tuple(), ('access_level', )) @exc.on_http_error(exc.GitlabUpdateError) def approve(self, access_level=gitlab.DEVELOPER_ACCESS, **kwargs): """Approve an access request. Args: access_level (int): The access level for the user **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabUpdateError: If the server fails to perform the request """ path = '%s/%s/approve' % (self.manager.path, self.id) data = {'access_level': access_level} server_data = self.manager.gitlab.http_put(path, post_data=data, **kwargs) self._update_attrs(server_data) class SubscribableMixin(object): @cli.register_custom_action(('ProjectIssue', 'ProjectMergeRequest', 'ProjectLabel')) @exc.on_http_error(exc.GitlabSubscribeError) def subscribe(self, **kwargs): """Subscribe to the object notifications. Args: **kwargs: Extra options to send to the server (e.g. sudo) raises: GitlabAuthenticationError: If authentication is not correct GitlabSubscribeError: If the subscription cannot be done """ path = '%s/%s/subscribe' % (self.manager.path, self.get_id()) server_data = self.manager.gitlab.http_post(path, **kwargs) self._update_attrs(server_data) @cli.register_custom_action(('ProjectIssue', 'ProjectMergeRequest', 'ProjectLabel')) @exc.on_http_error(exc.GitlabUnsubscribeError) def unsubscribe(self, **kwargs): """Unsubscribe from the object notifications. Args: **kwargs: Extra options to send to the server (e.g. sudo) raises: GitlabAuthenticationError: If authentication is not correct GitlabUnsubscribeError: If the unsubscription cannot be done """ path = '%s/%s/unsubscribe' % (self.manager.path, self.get_id()) server_data = self.manager.gitlab.http_post(path, **kwargs) self._update_attrs(server_data) class TodoMixin(object): @cli.register_custom_action(('ProjectIssue', 'ProjectMergeRequest')) @exc.on_http_error(exc.GitlabTodoError) def todo(self, **kwargs): """Create a todo associated to the object. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabTodoError: If the todo cannot be set """ path = '%s/%s/todo' % (self.manager.path, self.get_id()) self.manager.gitlab.http_post(path, **kwargs) class TimeTrackingMixin(object): @cli.register_custom_action(('ProjectIssue', 'ProjectMergeRequest')) @exc.on_http_error(exc.GitlabTimeTrackingError) def time_stats(self, **kwargs): """Get time stats for the object. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabTimeTrackingError: If the time tracking update cannot be done """ path = '%s/%s/time_stats' % (self.manager.path, self.get_id()) return self.manager.gitlab.http_get(path, **kwargs) @cli.register_custom_action(('ProjectIssue', 'ProjectMergeRequest'), ('duration', )) @exc.on_http_error(exc.GitlabTimeTrackingError) def time_estimate(self, duration, **kwargs): """Set an estimated time of work for the object. Args: duration (str): Duration in human format (e.g. 3h30) **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabTimeTrackingError: If the time tracking update cannot be done """ path = '%s/%s/time_estimate' % (self.manager.path, self.get_id()) data = {'duration': duration} return self.manager.gitlab.http_post(path, post_data=data, **kwargs) @cli.register_custom_action(('ProjectIssue', 'ProjectMergeRequest')) @exc.on_http_error(exc.GitlabTimeTrackingError) def reset_time_estimate(self, **kwargs): """Resets estimated time for the object to 0 seconds. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabTimeTrackingError: If the time tracking update cannot be done """ path = '%s/%s/rest_time_estimate' % (self.manager.path, self.get_id()) return self.manager.gitlab.http_post(path, **kwargs) @cli.register_custom_action(('ProjectIssue', 'ProjectMergeRequest'), ('duration', )) @exc.on_http_error(exc.GitlabTimeTrackingError) def add_spent_time(self, duration, **kwargs): """Add time spent working on the object. Args: duration (str): Duration in human format (e.g. 3h30) **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabTimeTrackingError: If the time tracking update cannot be done """ path = '%s/%s/add_spent_time' % (self.manager.path, self.get_id()) data = {'duration': duration} return self.manager.gitlab.http_post(path, post_data=data, **kwargs) @cli.register_custom_action(('ProjectIssue', 'ProjectMergeRequest')) @exc.on_http_error(exc.GitlabTimeTrackingError) def reset_spent_time(self, **kwargs): """Resets the time spent working on the object. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabTimeTrackingError: If the time tracking update cannot be done """ path = '%s/%s/reset_spent_time' % (self.manager.path, self.get_id()) return self.manager.gitlab.http_post(path, **kwargs) python-gitlab-1.3.0/gitlab/tests/000077500000000000000000000000001324224150200166735ustar00rootroot00000000000000python-gitlab-1.3.0/gitlab/tests/__init__.py000066400000000000000000000000001324224150200207720ustar00rootroot00000000000000python-gitlab-1.3.0/gitlab/tests/test_base.py000066400000000000000000000103031324224150200212130ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Copyright (C) 2017 Gauvain Pocentek # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . import pickle try: import unittest except ImportError: import unittest2 as unittest from gitlab import base class FakeGitlab(object): pass class FakeObject(base.RESTObject): pass class FakeManager(base.RESTManager): _obj_cls = FakeObject _path = '/tests' class TestRESTManager(unittest.TestCase): def test_computed_path_simple(self): class MGR(base.RESTManager): _path = '/tests' _obj_cls = object mgr = MGR(FakeGitlab()) self.assertEqual(mgr._computed_path, '/tests') def test_computed_path_with_parent(self): class MGR(base.RESTManager): _path = '/tests/%(test_id)s/cases' _obj_cls = object _from_parent_attrs = {'test_id': 'id'} class Parent(object): id = 42 class BrokenParent(object): no_id = 0 mgr = MGR(FakeGitlab(), parent=Parent()) self.assertEqual(mgr._computed_path, '/tests/42/cases') def test_path_property(self): class MGR(base.RESTManager): _path = '/tests' _obj_cls = object mgr = MGR(FakeGitlab()) self.assertEqual(mgr.path, '/tests') class TestRESTObject(unittest.TestCase): def setUp(self): self.gitlab = FakeGitlab() self.manager = FakeManager(self.gitlab) def test_instanciate(self): obj = FakeObject(self.manager, {'foo': 'bar'}) self.assertDictEqual({'foo': 'bar'}, obj._attrs) self.assertDictEqual({}, obj._updated_attrs) self.assertEqual(None, obj._create_managers()) self.assertEqual(self.manager, obj.manager) self.assertEqual(self.gitlab, obj.manager.gitlab) def test_pickability(self): obj = FakeObject(self.manager, {'foo': 'bar'}) original_obj_module = obj._module pickled = pickle.dumps(obj) unpickled = pickle.loads(pickled) self.assertIsInstance(unpickled, FakeObject) self.assertTrue(hasattr(unpickled, '_module')) self.assertEqual(unpickled._module, original_obj_module) def test_attrs(self): obj = FakeObject(self.manager, {'foo': 'bar'}) self.assertEqual('bar', obj.foo) self.assertRaises(AttributeError, getattr, obj, 'bar') obj.bar = 'baz' self.assertEqual('baz', obj.bar) self.assertDictEqual({'foo': 'bar'}, obj._attrs) self.assertDictEqual({'bar': 'baz'}, obj._updated_attrs) def test_get_id(self): obj = FakeObject(self.manager, {'foo': 'bar'}) obj.id = 42 self.assertEqual(42, obj.get_id()) obj.id = None self.assertEqual(None, obj.get_id()) def test_custom_id_attr(self): class OtherFakeObject(FakeObject): _id_attr = 'foo' obj = OtherFakeObject(self.manager, {'foo': 'bar'}) self.assertEqual('bar', obj.get_id()) def test_update_attrs(self): obj = FakeObject(self.manager, {'foo': 'bar'}) obj.bar = 'baz' obj._update_attrs({'foo': 'foo', 'bar': 'bar'}) self.assertDictEqual({'foo': 'foo', 'bar': 'bar'}, obj._attrs) self.assertDictEqual({}, obj._updated_attrs) def test_create_managers(self): class ObjectWithManager(FakeObject): _managers = (('fakes', 'FakeManager'), ) obj = ObjectWithManager(self.manager, {'foo': 'bar'}) self.assertIsInstance(obj.fakes, FakeManager) self.assertEqual(obj.fakes.gitlab, self.gitlab) self.assertEqual(obj.fakes._parent, obj) python-gitlab-1.3.0/gitlab/tests/test_cli.py000066400000000000000000000070101324224150200210510ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright (C) 2016-2017 Gauvain Pocentek # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . from __future__ import print_function from __future__ import absolute_import import argparse import six try: import unittest except ImportError: import unittest2 as unittest from gitlab import cli import gitlab.v3.cli class TestCLI(unittest.TestCase): def test_what_to_cls(self): self.assertEqual("Foo", cli.what_to_cls("foo")) self.assertEqual("FooBar", cli.what_to_cls("foo-bar")) def test_cls_to_what(self): class Class(object): pass class TestClass(object): pass self.assertEqual("test-class", cli.cls_to_what(TestClass)) self.assertEqual("class", cli.cls_to_what(Class)) def test_die(self): with self.assertRaises(SystemExit) as test: cli.die("foobar") self.assertEqual(test.exception.code, 1) def test_base_parser(self): parser = cli._get_base_parser() args = parser.parse_args(['-v', '-g', 'gl_id', '-c', 'foo.cfg', '-c', 'bar.cfg']) self.assertTrue(args.verbose) self.assertEqual(args.gitlab, 'gl_id') self.assertEqual(args.config_file, ['foo.cfg', 'bar.cfg']) class TestV3CLI(unittest.TestCase): def test_parse_args(self): parser = cli._get_parser(gitlab.v3.cli) args = parser.parse_args(['project', 'list']) self.assertEqual(args.what, 'project') self.assertEqual(args.action, 'list') def test_parser(self): parser = cli._get_parser(gitlab.v3.cli) subparsers = None for action in parser._actions: if type(action) == argparse._SubParsersAction: subparsers = action break self.assertIsNotNone(subparsers) self.assertIn('user', subparsers.choices) user_subparsers = None for action in subparsers.choices['user']._actions: if type(action) == argparse._SubParsersAction: user_subparsers = action break self.assertIsNotNone(user_subparsers) self.assertIn('list', user_subparsers.choices) self.assertIn('get', user_subparsers.choices) self.assertIn('delete', user_subparsers.choices) self.assertIn('update', user_subparsers.choices) self.assertIn('create', user_subparsers.choices) self.assertIn('block', user_subparsers.choices) self.assertIn('unblock', user_subparsers.choices) actions = user_subparsers.choices['create']._option_string_actions self.assertFalse(actions['--twitter'].required) self.assertTrue(actions['--username'].required) def test_extra_actions(self): for cls, data in six.iteritems(gitlab.v3.cli.EXTRA_ACTIONS): for key in data: self.assertIsInstance(data[key], dict) python-gitlab-1.3.0/gitlab/tests/test_config.py000066400000000000000000000106041324224150200215520ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Copyright (C) 2016-2017 Gauvain Pocentek # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . try: import unittest except ImportError: import unittest2 as unittest import mock import six from gitlab import config valid_config = u"""[global] default = one ssl_verify = true timeout = 2 [one] url = http://one.url private_token = ABCDEF [two] url = https://two.url private_token = GHIJKL ssl_verify = false timeout = 10 [three] url = https://three.url private_token = MNOPQR ssl_verify = /path/to/CA/bundle.crt [four] url = https://four.url oauth_token = STUV """ no_default_config = u"""[global] [there] url = http://there.url private_token = ABCDEF """ missing_attr_config = u"""[global] [one] url = http://one.url [two] private_token = ABCDEF [three] meh = hem """ class TestConfigParser(unittest.TestCase): @mock.patch('six.moves.builtins.open') def test_invalid_id(self, m_open): fd = six.StringIO(no_default_config) fd.close = mock.Mock(return_value=None) m_open.return_value = fd self.assertRaises(config.GitlabIDError, config.GitlabConfigParser) fd = six.StringIO(valid_config) fd.close = mock.Mock(return_value=None) m_open.return_value = fd self.assertRaises(config.GitlabDataError, config.GitlabConfigParser, gitlab_id='not_there') @mock.patch('six.moves.builtins.open') def test_invalid_data(self, m_open): fd = six.StringIO(missing_attr_config) fd.close = mock.Mock(return_value=None) m_open.return_value = fd config.GitlabConfigParser('one') self.assertRaises(config.GitlabDataError, config.GitlabConfigParser, gitlab_id='two') self.assertRaises(config.GitlabDataError, config.GitlabConfigParser, gitlab_id='three') @mock.patch('six.moves.builtins.open') def test_valid_data(self, m_open): fd = six.StringIO(valid_config) fd.close = mock.Mock(return_value=None) m_open.return_value = fd cp = config.GitlabConfigParser() self.assertEqual("one", cp.gitlab_id) self.assertEqual("http://one.url", cp.url) self.assertEqual("ABCDEF", cp.private_token) self.assertEqual(None, cp.oauth_token) self.assertEqual(2, cp.timeout) self.assertEqual(True, cp.ssl_verify) fd = six.StringIO(valid_config) fd.close = mock.Mock(return_value=None) m_open.return_value = fd cp = config.GitlabConfigParser(gitlab_id="two") self.assertEqual("two", cp.gitlab_id) self.assertEqual("https://two.url", cp.url) self.assertEqual("GHIJKL", cp.private_token) self.assertEqual(None, cp.oauth_token) self.assertEqual(10, cp.timeout) self.assertEqual(False, cp.ssl_verify) fd = six.StringIO(valid_config) fd.close = mock.Mock(return_value=None) m_open.return_value = fd cp = config.GitlabConfigParser(gitlab_id="three") self.assertEqual("three", cp.gitlab_id) self.assertEqual("https://three.url", cp.url) self.assertEqual("MNOPQR", cp.private_token) self.assertEqual(None, cp.oauth_token) self.assertEqual(2, cp.timeout) self.assertEqual("/path/to/CA/bundle.crt", cp.ssl_verify) fd = six.StringIO(valid_config) fd.close = mock.Mock(return_value=None) m_open.return_value = fd cp = config.GitlabConfigParser(gitlab_id="four") self.assertEqual("four", cp.gitlab_id) self.assertEqual("https://four.url", cp.url) self.assertEqual(None, cp.private_token) self.assertEqual("STUV", cp.oauth_token) self.assertEqual(2, cp.timeout) self.assertEqual(True, cp.ssl_verify) python-gitlab-1.3.0/gitlab/tests/test_gitlab.py000066400000000000000000001346221324224150200215560ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Copyright (C) 2014 Mika Mäenpää , # Tampere University of Technology # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . from __future__ import print_function import pickle try: import unittest except ImportError: import unittest2 as unittest from httmock import HTTMock # noqa from httmock import response # noqa from httmock import urlmatch # noqa import requests import six import gitlab from gitlab import * # noqa class TestSanitize(unittest.TestCase): def test_do_nothing(self): self.assertEqual(1, gitlab._sanitize(1)) self.assertEqual(1.5, gitlab._sanitize(1.5)) self.assertEqual("foo", gitlab._sanitize("foo")) def test_slash(self): self.assertEqual("foo%2Fbar", gitlab._sanitize("foo/bar")) def test_dict(self): source = {"url": "foo/bar", "id": 1} expected = {"url": "foo%2Fbar", "id": 1} self.assertEqual(expected, gitlab._sanitize(source)) 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, api_version=3) @urlmatch(scheme="http", netloc="localhost", path="/api/v3/known_path", method="get") def resp_get(self, url, request): headers = {'content-type': 'application/json'} content = 'response'.encode("utf-8") return response(200, content, headers, None, 5, request) def test_raw_get_unknown_path(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v3/unknown_path", method="get") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = '{"message": "message"}'.encode("utf-8") return response(404, content, headers, None, 5, request) with HTTMock(resp_cont): resp = self.gl._raw_get("/unknown_path") self.assertEqual(resp.status_code, 404) def test_raw_get_without_kwargs(self): with HTTMock(self.resp_get): resp = self.gl._raw_get("/known_path") self.assertEqual(resp.content, b'response') self.assertEqual(resp.status_code, 200) def test_raw_get_with_kwargs(self): with HTTMock(self.resp_get): resp = self.gl._raw_get("/known_path", sudo="testing") self.assertEqual(resp.content, b'response') self.assertEqual(resp.status_code, 200) def test_raw_post(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v3/known_path", method="post") def resp_post(url, request): headers = {'content-type': 'application/json'} content = 'response'.encode("utf-8") return response(200, content, headers, None, 5, request) with HTTMock(resp_post): resp = self.gl._raw_post("/known_path") self.assertEqual(resp.content, b'response') self.assertEqual(resp.status_code, 200) def test_raw_post_unknown_path(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v3/unknown_path", method="post") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = '{"message": "message"}'.encode("utf-8") return response(404, content, headers, None, 5, request) with HTTMock(resp_cont): resp = self.gl._raw_post("/unknown_path") self.assertEqual(resp.status_code, 404) def test_raw_put(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v3/known_path", method="put") def resp_put(url, request): headers = {'content-type': 'application/json'} content = 'response'.encode("utf-8") return response(200, content, headers, None, 5, request) with HTTMock(resp_put): resp = self.gl._raw_put("/known_path") self.assertEqual(resp.content, b'response') self.assertEqual(resp.status_code, 200) def test_raw_put_unknown_path(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v3/unknown_path", method="put") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = '{"message": "message"}'.encode("utf-8") return response(404, content, headers, None, 5, request) with HTTMock(resp_cont): resp = self.gl._raw_put("/unknown_path") self.assertEqual(resp.status_code, 404) def test_raw_delete(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v3/known_path", method="delete") def resp_delete(url, request): headers = {'content-type': 'application/json'} content = 'response'.encode("utf-8") return response(200, content, headers, None, 5, request) with HTTMock(resp_delete): resp = self.gl._raw_delete("/known_path") self.assertEqual(resp.content, b'response') self.assertEqual(resp.status_code, 200) def test_raw_delete_unknown_path(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v3/unknown_path", method="delete") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = '{"message": "message"}'.encode("utf-8") return response(404, content, headers, None, 5, request) with HTTMock(resp_cont): resp = self.gl._raw_delete("/unknown_path") self.assertEqual(resp.status_code, 404) class TestGitlabList(unittest.TestCase): def setUp(self): self.gl = Gitlab("http://localhost", private_token="private_token", api_version=4) def test_build_list(self): @urlmatch(scheme='http', netloc="localhost", path="/api/v4/tests", method="get") def resp_1(url, request): headers = {'content-type': 'application/json', 'X-Page': 1, 'X-Next-Page': 2, 'X-Per-Page': 1, 'X-Total-Pages': 2, 'X-Total': 2, 'Link': ( ';' ' rel="next"')} content = '[{"a": "b"}]' return response(200, content, headers, None, 5, request) @urlmatch(scheme='http', netloc="localhost", path="/api/v4/tests", method='get', query=r'.*page=2') def resp_2(url, request): headers = {'content-type': 'application/json', 'X-Page': 2, 'X-Next-Page': 2, 'X-Per-Page': 1, 'X-Total-Pages': 2, 'X-Total': 2} content = '[{"c": "d"}]' return response(200, content, headers, None, 5, request) with HTTMock(resp_1): obj = self.gl.http_list('/tests', as_list=False) self.assertEqual(len(obj), 2) self.assertEqual(obj._next_url, 'http://localhost/api/v4/tests?per_page=1&page=2') self.assertEqual(obj.current_page, 1) self.assertEqual(obj.prev_page, None) self.assertEqual(obj.next_page, 2) self.assertEqual(obj.per_page, 1) self.assertEqual(obj.total_pages, 2) self.assertEqual(obj.total, 2) with HTTMock(resp_2): l = list(obj) self.assertEqual(len(l), 2) self.assertEqual(l[0]['a'], 'b') self.assertEqual(l[1]['c'], 'd') class TestGitlabHttpMethods(unittest.TestCase): def setUp(self): self.gl = Gitlab("http://localhost", private_token="private_token", api_version=4) def test_build_url(self): r = self.gl._build_url('http://localhost/api/v4') self.assertEqual(r, 'http://localhost/api/v4') r = self.gl._build_url('https://localhost/api/v4') self.assertEqual(r, 'https://localhost/api/v4') r = self.gl._build_url('/projects') self.assertEqual(r, 'http://localhost/api/v4/projects') def test_http_request(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects", method="get") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = '[{"name": "project1"}]' return response(200, content, headers, None, 5, request) with HTTMock(resp_cont): http_r = self.gl.http_request('get', '/projects') http_r.json() self.assertEqual(http_r.status_code, 200) def test_http_request_404(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v4/not_there", method="get") def resp_cont(url, request): content = {'Here is wh it failed'} return response(404, content, {}, None, 5, request) with HTTMock(resp_cont): self.assertRaises(GitlabHttpError, self.gl.http_request, 'get', '/not_there') def test_get_request(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects", method="get") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = '{"name": "project1"}' return response(200, content, headers, None, 5, request) with HTTMock(resp_cont): result = self.gl.http_get('/projects') self.assertIsInstance(result, dict) self.assertEqual(result['name'], 'project1') def test_get_request_raw(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects", method="get") def resp_cont(url, request): headers = {'content-type': 'application/octet-stream'} content = 'content' return response(200, content, headers, None, 5, request) with HTTMock(resp_cont): result = self.gl.http_get('/projects') self.assertEqual(result.content.decode('utf-8'), 'content') def test_get_request_404(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v4/not_there", method="get") def resp_cont(url, request): content = {'Here is wh it failed'} return response(404, content, {}, None, 5, request) with HTTMock(resp_cont): self.assertRaises(GitlabHttpError, self.gl.http_get, '/not_there') def test_get_request_invalid_data(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects", method="get") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = '["name": "project1"]' return response(200, content, headers, None, 5, request) with HTTMock(resp_cont): self.assertRaises(GitlabParsingError, self.gl.http_get, '/projects') def test_list_request(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects", method="get") def resp_cont(url, request): headers = {'content-type': 'application/json', 'X-Total': 1} content = '[{"name": "project1"}]' return response(200, content, headers, None, 5, request) with HTTMock(resp_cont): result = self.gl.http_list('/projects', as_list=True) self.assertIsInstance(result, list) self.assertEqual(len(result), 1) with HTTMock(resp_cont): result = self.gl.http_list('/projects', as_list=False) self.assertIsInstance(result, GitlabList) self.assertEqual(len(result), 1) with HTTMock(resp_cont): result = self.gl.http_list('/projects', all=True) self.assertIsInstance(result, list) self.assertEqual(len(result), 1) def test_list_request_404(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v4/not_there", method="get") def resp_cont(url, request): content = {'Here is why it failed'} return response(404, content, {}, None, 5, request) with HTTMock(resp_cont): self.assertRaises(GitlabHttpError, self.gl.http_list, '/not_there') def test_list_request_invalid_data(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects", method="get") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = '["name": "project1"]' return response(200, content, headers, None, 5, request) with HTTMock(resp_cont): self.assertRaises(GitlabParsingError, self.gl.http_list, '/projects') def test_post_request(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects", method="post") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = '{"name": "project1"}' return response(200, content, headers, None, 5, request) with HTTMock(resp_cont): result = self.gl.http_post('/projects') self.assertIsInstance(result, dict) self.assertEqual(result['name'], 'project1') def test_post_request_404(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v4/not_there", method="post") def resp_cont(url, request): content = {'Here is wh it failed'} return response(404, content, {}, None, 5, request) with HTTMock(resp_cont): self.assertRaises(GitlabHttpError, self.gl.http_post, '/not_there') def test_post_request_invalid_data(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects", method="post") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = '["name": "project1"]' return response(200, content, headers, None, 5, request) with HTTMock(resp_cont): self.assertRaises(GitlabParsingError, self.gl.http_post, '/projects') def test_put_request(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects", method="put") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = '{"name": "project1"}' return response(200, content, headers, None, 5, request) with HTTMock(resp_cont): result = self.gl.http_put('/projects') self.assertIsInstance(result, dict) self.assertEqual(result['name'], 'project1') def test_put_request_404(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v4/not_there", method="put") def resp_cont(url, request): content = {'Here is wh it failed'} return response(404, content, {}, None, 5, request) with HTTMock(resp_cont): self.assertRaises(GitlabHttpError, self.gl.http_put, '/not_there') def test_put_request_invalid_data(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects", method="put") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = '["name": "project1"]' return response(200, content, headers, None, 5, request) with HTTMock(resp_cont): self.assertRaises(GitlabParsingError, self.gl.http_put, '/projects') def test_delete_request(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects", method="delete") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = 'true' return response(200, content, headers, None, 5, request) with HTTMock(resp_cont): result = self.gl.http_delete('/projects') self.assertIsInstance(result, requests.Response) self.assertEqual(result.json(), True) def test_delete_request_404(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v4/not_there", method="delete") def resp_cont(url, request): content = {'Here is wh it failed'} return response(404, content, {}, None, 5, request) with HTTMock(resp_cont): self.assertRaises(GitlabHttpError, self.gl.http_delete, '/not_there') 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, api_version=3) def test_list(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v3/projects/1/repository/branches", method="get") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = ('[{"branch_name": "testbranch", ' '"project_id": 1, "ref": "a"}]').encode("utf-8") return response(200, content, headers, None, 5, request) with HTTMock(resp_cont): data = self.gl.list(ProjectBranch, project_id=1, page=1, per_page=20) self.assertEqual(len(data), 1) data = data[0] self.assertEqual(data.branch_name, "testbranch") self.assertEqual(data.project_id, 1) self.assertEqual(data.ref, "a") def test_list_next_link(self): @urlmatch(scheme="http", netloc="localhost", path='/api/v3/projects/1/repository/branches', method="get") def resp_one(url, request): """First request: http://localhost/api/v3/projects/1/repository/branches?per_page=1 """ headers = { 'content-type': 'application/json', 'link': '; rel="next", ; rel="las' 't", ; rel="first"' } content = ('[{"branch_name": "otherbranch", ' '"project_id": 1, "ref": "b"}]').encode("utf-8") resp = response(200, content, headers, None, 5, request) return resp @urlmatch(scheme="http", netloc="localhost", path='/api/v3/projects/1/repository/branches', method="get", query=r'.*page=2.*') def resp_two(url, request): headers = { 'content-type': 'application/json', 'link': '; rel="prev", ; rel="las' 't", ; rel="first"' } content = ('[{"branch_name": "testbranch", ' '"project_id": 1, "ref": "a"}]').encode("utf-8") resp = response(200, content, headers, None, 5, request) return resp with HTTMock(resp_two, resp_one): data = self.gl.list(ProjectBranch, project_id=1, per_page=1, all=True) self.assertEqual(data[1].branch_name, "testbranch") self.assertEqual(data[1].project_id, 1) self.assertEqual(data[1].ref, "a") self.assertEqual(data[0].branch_name, "otherbranch") self.assertEqual(data[0].project_id, 1) self.assertEqual(data[0].ref, "b") self.assertEqual(len(data), 2) def test_list_recursion_limit_caught(self): @urlmatch(scheme="http", netloc="localhost", path='/api/v3/projects/1/repository/branches', method="get") def resp_one(url, request): """First request: http://localhost/api/v3/projects/1/repository/branches?per_page=1 """ headers = { 'content-type': 'application/json', 'link': '; rel="next", ; rel="las' 't", ; rel="first"' } content = ('[{"branch_name": "otherbranch", ' '"project_id": 1, "ref": "b"}]').encode("utf-8") resp = response(200, content, headers, None, 5, request) return resp @urlmatch(scheme="http", netloc="localhost", path='/api/v3/projects/1/repository/branches', method="get", query=r'.*page=2.*') def resp_two(url, request): # Mock a runtime error raise RuntimeError("maximum recursion depth exceeded") with HTTMock(resp_two, resp_one): data = self.gl.list(ProjectBranch, project_id=1, per_page=1, safe_all=True) self.assertEqual(data[0].branch_name, "otherbranch") self.assertEqual(data[0].project_id, 1) self.assertEqual(data[0].ref, "b") self.assertEqual(len(data), 1) def test_list_recursion_limit_not_caught(self): @urlmatch(scheme="http", netloc="localhost", path='/api/v3/projects/1/repository/branches', method="get") def resp_one(url, request): """First request: http://localhost/api/v3/projects/1/repository/branches?per_page=1 """ headers = { 'content-type': 'application/json', 'link': '; rel="next", ; rel="las' 't", ; rel="first"' } content = ('[{"branch_name": "otherbranch", ' '"project_id": 1, "ref": "b"}]').encode("utf-8") resp = response(200, content, headers, None, 5, request) return resp @urlmatch(scheme="http", netloc="localhost", path='/api/v3/projects/1/repository/branches', method="get", query=r'.*page=2.*') def resp_two(url, request): # Mock a runtime error raise RuntimeError("maximum recursion depth exceeded") with HTTMock(resp_two, resp_one): with six.assertRaisesRegex(self, GitlabError, "(maximum recursion depth exceeded)"): self.gl.list(ProjectBranch, project_id=1, per_page=1, all=True) def test_list_401(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v3/projects/1/repository/branches", method="get") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = '{"message":"message"}'.encode("utf-8") return response(401, content, headers, None, 5, request) with HTTMock(resp_cont): self.assertRaises(GitlabAuthenticationError, self.gl.list, ProjectBranch, project_id=1) def test_list_unknown_error(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v3/projects/1/repository/branches", method="get") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = '{"message":"message"}'.encode("utf-8") return response(405, content, headers, None, 5, request) with HTTMock(resp_cont): self.assertRaises(GitlabListError, self.gl.list, ProjectBranch, project_id=1) def test_list_kw_missing(self): self.assertRaises(GitlabListError, self.gl.list, ProjectBranch) def test_list_no_connection(self): self.gl._url = 'http://localhost:66000/api/v3' self.assertRaises(GitlabConnectionError, self.gl.list, ProjectBranch, project_id=1) def test_get(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v3/projects/1", method="get") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = '{"name": "testproject"}'.encode("utf-8") return response(200, content, headers, None, 5, request) with HTTMock(resp_cont): data = self.gl.get(Project, id=1) expected = {"name": "testproject"} self.assertEqual(expected, data) def test_get_unknown_path(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v3/groups/1", method="get") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = '{"message": "message"}'.encode("utf-8") return response(404, content, headers, None, 5, request) with HTTMock(resp_cont): self.assertRaises(GitlabGetError, self.gl.get, Group, 1) def test_get_missing_kw(self): self.assertRaises(GitlabGetError, self.gl.get, ProjectBranch) def test_get_401(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v3/projects/1", method="get") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = '{"message": "message"}'.encode("utf-8") return response(401, content, headers, None, 5, request) with HTTMock(resp_cont): self.assertRaises(GitlabAuthenticationError, self.gl.get, Project, 1) def test_get_404(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v3/projects/1", method="get") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = '{"message": "message"}'.encode("utf-8") return response(404, content, headers, None, 5, request) with HTTMock(resp_cont): self.assertRaises(GitlabGetError, self.gl.get, Project, 1) def test_get_unknown_error(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v3/projects/1", method="get") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = '{"message": "message"}'.encode("utf-8") return response(405, content, headers, None, 5, request) with HTTMock(resp_cont): self.assertRaises(GitlabGetError, self.gl.get, Project, 1) def test_delete_from_object(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v3/groups/1", method="delete") def resp_delete_group(url, request): headers = {'content-type': 'application/json'} content = ''.encode("utf-8") return response(200, content, headers, None, 5, request) obj = Group(self.gl, data={"name": "testname", "id": 1}) with HTTMock(resp_delete_group): data = self.gl.delete(obj) self.assertIs(data, True) def test_delete_from_invalid_class(self): class InvalidClass(object): pass self.assertRaises(GitlabError, self.gl.delete, InvalidClass, 1) def test_delete_from_class(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v3/groups/1", method="delete") def resp_delete_group(url, request): headers = {'content-type': 'application/json'} content = ''.encode("utf-8") return response(200, content, headers, None, 5, request) with HTTMock(resp_delete_group): data = self.gl.delete(Group, 1) self.assertIs(data, True) def test_delete_unknown_path(self): obj = Project(self.gl, data={"name": "testname", "id": 1}) obj._from_api = True @urlmatch(scheme="http", netloc="localhost", path="/api/v3/projects/1", method="delete") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = '{"message": "message"}'.encode("utf-8") return response(404, content, headers, None, 5, request) with HTTMock(resp_cont): self.assertRaises(GitlabDeleteError, self.gl.delete, obj) def test_delete_401(self): obj = Project(self.gl, data={"name": "testname", "id": 1}) @urlmatch(scheme="http", netloc="localhost", path="/api/v3/projects/1", method="delete") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = '{"message": "message"}'.encode("utf-8") return response(401, content, headers, None, 5, request) with HTTMock(resp_cont): self.assertRaises(GitlabAuthenticationError, self.gl.delete, obj) def test_delete_unknown_error(self): obj = Project(self.gl, data={"name": "testname", "id": 1}) @urlmatch(scheme="http", netloc="localhost", path="/api/v3/projects/1", method="delete") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = '{"message": "message"}'.encode("utf-8") return response(405, content, headers, None, 5, request) with HTTMock(resp_cont): self.assertRaises(GitlabDeleteError, self.gl.delete, obj) def test_create(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v3/projects", method="post") def resp_create_project(url, request): headers = {'content-type': 'application/json'} content = '{"name": "testname", "id": 1}'.encode("utf-8") return response(201, content, headers, None, 5, request) obj = Project(self.gl, data={"name": "testname"}) with HTTMock(resp_create_project): data = self.gl.create(obj) expected = {u"name": u"testname", u"id": 1} self.assertEqual(expected, data) def test_create_kw_missing(self): obj = Group(self.gl, data={"name": "testgroup"}) self.assertRaises(GitlabCreateError, self.gl.create, obj) def test_create_unknown_path(self): obj = Project(self.gl, data={"name": "name"}) obj.id = 1 obj._from_api = True @urlmatch(scheme="http", netloc="localhost", path="/api/v3/projects/1", method="delete") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = '{"message": "message"}'.encode("utf-8") return response(404, content, headers, None, 5, request) with HTTMock(resp_cont): self.assertRaises(GitlabDeleteError, self.gl.delete, obj) def test_create_401(self): obj = Group(self.gl, data={"name": "testgroup", "path": "testpath"}) @urlmatch(scheme="http", netloc="localhost", path="/api/v3/groups", method="post") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = '{"message": "message"}'.encode("utf-8") return response(401, content, headers, None, 5, request) with HTTMock(resp_cont): self.assertRaises(GitlabAuthenticationError, self.gl.create, obj) def test_create_unknown_error(self): obj = Group(self.gl, data={"name": "testgroup", "path": "testpath"}) @urlmatch(scheme="http", netloc="localhost", path="/api/v3/groups", method="post") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = '{"message": "message"}'.encode("utf-8") return response(405, content, headers, None, 5, request) with HTTMock(resp_cont): self.assertRaises(GitlabCreateError, self.gl.create, obj) def test_update(self): obj = User(self.gl, data={"email": "testuser@testmail.com", "password": "testpassword", "name": u"testuser", "username": "testusername", "can_create_group": True, "id": 1}) @urlmatch(scheme="http", netloc="localhost", path="/api/v3/users/1", method="put") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = '{"first": "return1"}'.encode("utf-8") return response(200, content, headers, None, 5, request) with HTTMock(resp_cont): data = self.gl.update(obj) expected = {"first": "return1"} self.assertEqual(expected, data) def test_update_kw_missing(self): obj = Hook(self.gl, data={"name": "testgroup"}) self.assertRaises(GitlabUpdateError, self.gl.update, obj) def test_update_401(self): obj = Group(self.gl, data={"name": "testgroup", "path": "testpath", "id": 1}) @urlmatch(scheme="http", netloc="localhost", path="/api/v3/groups/1", method="put") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = '{"message": "message"}'.encode("utf-8") return response(401, content, headers, None, 5, request) with HTTMock(resp_cont): self.assertRaises(GitlabAuthenticationError, self.gl.update, obj) def test_update_unknown_error(self): obj = Group(self.gl, data={"name": "testgroup", "path": "testpath", "id": 1}) @urlmatch(scheme="http", netloc="localhost", path="/api/v3/groups/1", method="put") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = '{"message": "message"}'.encode("utf-8") return response(405, content, headers, None, 5, request) with HTTMock(resp_cont): self.assertRaises(GitlabUpdateError, self.gl.update, obj) def test_update_unknown_path(self): obj = Group(self.gl, data={"name": "testgroup", "path": "testpath", "id": 1}) @urlmatch(scheme="http", netloc="localhost", path="/api/v3/groups/1", method="put") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = '{"message": "message"}'.encode("utf-8") return response(404, content, headers, None, 5, request) with HTTMock(resp_cont): self.assertRaises(GitlabUpdateError, self.gl.update, obj) class TestGitlabAuth(unittest.TestCase): def test_invalid_auth_args(self): self.assertRaises(ValueError, Gitlab, "http://localhost", api_version='4', private_token='private_token', oauth_token='bearer') self.assertRaises(ValueError, Gitlab, "http://localhost", api_version='4', oauth_token='bearer', http_username='foo', http_password='bar') self.assertRaises(ValueError, Gitlab, "http://localhost", api_version='4', private_token='private_token', http_password='bar') self.assertRaises(ValueError, Gitlab, "http://localhost", api_version='4', private_token='private_token', http_username='foo') def test_private_token_auth(self): gl = Gitlab('http://localhost', private_token='private_token', api_version='4') self.assertEqual(gl.private_token, 'private_token') self.assertEqual(gl.oauth_token, None) self.assertEqual(gl._http_auth, None) self.assertEqual(gl.headers['PRIVATE-TOKEN'], 'private_token') self.assertNotIn('Authorization', gl.headers) def test_oauth_token_auth(self): gl = Gitlab('http://localhost', oauth_token='oauth_token', api_version='4') self.assertEqual(gl.private_token, None) self.assertEqual(gl.oauth_token, 'oauth_token') self.assertEqual(gl._http_auth, None) self.assertEqual(gl.headers['Authorization'], 'Bearer oauth_token') self.assertNotIn('PRIVATE-TOKEN', gl.headers) def test_http_auth(self): gl = Gitlab('http://localhost', private_token='private_token', http_username='foo', http_password='bar', api_version='4') self.assertEqual(gl.private_token, 'private_token') self.assertEqual(gl.oauth_token, None) self.assertIsInstance(gl._http_auth, requests.auth.HTTPBasicAuth) self.assertEqual(gl.headers['PRIVATE-TOKEN'], 'private_token') self.assertNotIn('Authorization', gl.headers) 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, api_version=3) def test_pickability(self): original_gl_objects = self.gl._objects pickled = pickle.dumps(self.gl) unpickled = pickle.loads(pickled) self.assertIsInstance(unpickled, Gitlab) self.assertTrue(hasattr(unpickled, '_objects')) self.assertEqual(unpickled._objects, original_gl_objects) def test_credentials_auth_nopassword(self): self.gl.email = None self.gl.password = None @urlmatch(scheme="http", netloc="localhost", path="/api/v3/session", method="post") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = '{"message": "message"}'.encode("utf-8") return response(404, content, headers, None, 5, request) with HTTMock(resp_cont): self.assertRaises(GitlabAuthenticationError, self.gl._credentials_auth) def test_credentials_auth_notok(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v3/session", method="post") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = '{"message": "message"}'.encode("utf-8") return response(404, content, headers, None, 5, request) with HTTMock(resp_cont): self.assertRaises(GitlabAuthenticationError, self.gl._credentials_auth) def test_auth_with_credentials(self): self.gl.private_token = None self.test_credentials_auth(callback=self.gl.auth) def test_auth_with_token(self): self.test_token_auth(callback=self.gl.auth) def test_credentials_auth(self, callback=None): if callback is None: callback = self.gl._credentials_auth token = "credauthtoken" id_ = 1 expected = {"PRIVATE-TOKEN": token} @urlmatch(scheme="http", netloc="localhost", path="/api/v3/session", method="post") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = '{{"id": {0:d}, "private_token": "{1:s}"}}'.format( id_, token).encode("utf-8") return response(201, content, headers, None, 5, request) with HTTMock(resp_cont): callback() self.assertEqual(self.gl.private_token, token) self.assertDictContainsSubset(expected, self.gl.headers) self.assertEqual(self.gl.user.id, id_) def test_token_auth(self, callback=None): if callback is None: callback = self.gl._token_auth name = "username" id_ = 1 @urlmatch(scheme="http", netloc="localhost", path="/api/v3/user", method="get") def resp_cont(url, request): headers = {'content-type': 'application/json'} content = '{{"id": {0:d}, "username": "{1:s}"}}'.format( id_, name).encode("utf-8") return response(200, content, headers, None, 5, request) with HTTMock(resp_cont): callback() self.assertEqual(self.gl.user.username, name) self.assertEqual(self.gl.user.id, id_) self.assertEqual(type(self.gl.user), CurrentUser) def test_hooks(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v3/hooks/1", method="get") def resp_get_hook(url, request): headers = {'content-type': 'application/json'} content = '{"url": "testurl", "id": 1}'.encode("utf-8") return response(200, content, headers, None, 5, request) with HTTMock(resp_get_hook): data = self.gl.hooks.get(1) self.assertEqual(type(data), Hook) self.assertEqual(data.url, "testurl") self.assertEqual(data.id, 1) def test_projects(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v3/projects/1", method="get") def resp_get_project(url, request): headers = {'content-type': 'application/json'} content = '{"name": "name", "id": 1}'.encode("utf-8") return response(200, content, headers, None, 5, request) with HTTMock(resp_get_project): data = self.gl.projects.get(1) self.assertEqual(type(data), Project) self.assertEqual(data.name, "name") self.assertEqual(data.id, 1) def test_userprojects(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v3/projects/user/2", method="get") def resp_get_userproject(url, request): headers = {'content-type': 'application/json'} content = '{"name": "name", "id": 1, "user_id": 2}'.encode("utf-8") return response(200, content, headers, None, 5, request) with HTTMock(resp_get_userproject): self.assertRaises(NotImplementedError, self.gl.user_projects.get, 1, user_id=2) def test_groups(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v3/groups/1", method="get") def resp_get_group(url, request): headers = {'content-type': 'application/json'} content = '{"name": "name", "id": 1, "path": "path"}' content = content.encode('utf-8') return response(200, content, headers, None, 5, request) with HTTMock(resp_get_group): data = self.gl.groups.get(1) self.assertEqual(type(data), Group) self.assertEqual(data.name, "name") self.assertEqual(data.path, "path") self.assertEqual(data.id, 1) def test_issues(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v3/issues", method="get") def resp_get_issue(url, request): headers = {'content-type': 'application/json'} content = ('[{"name": "name", "id": 1}, ' '{"name": "other_name", "id": 2}]') content = content.encode("utf-8") return response(200, content, headers, None, 5, request) with HTTMock(resp_get_issue): data = self.gl.issues.get(2) self.assertEqual(data.id, 2) self.assertEqual(data.name, 'other_name') def test_users(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v3/users/1", method="get") def resp_get_user(url, request): headers = {'content-type': 'application/json'} content = ('{"name": "name", "id": 1, "password": "password", ' '"username": "username", "email": "email"}') content = content.encode("utf-8") return response(200, content, headers, None, 5, request) with HTTMock(resp_get_user): user = self.gl.users.get(1) self.assertEqual(type(user), User) self.assertEqual(user.name, "name") self.assertEqual(user.id, 1) def test_teams(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v3/user_teams/1", method="get") def resp_get_group(url, request): headers = {'content-type': 'application/json'} content = '{"name": "name", "id": 1, "path": "path"}' content = content.encode('utf-8') return response(200, content, headers, None, 5, request) with HTTMock(resp_get_group): data = self.gl.teams.get(1) self.assertEqual(type(data), Team) self.assertEqual(data.name, "name") self.assertEqual(data.path, "path") self.assertEqual(data.id, 1) python-gitlab-1.3.0/gitlab/tests/test_gitlabobject.py000066400000000000000000000462021324224150200227410ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright (C) 2014 Mika Mäenpää # Tampere University of Technology # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . from __future__ import print_function from __future__ import absolute_import import json import pickle try: import unittest except ImportError: import unittest2 as unittest from httmock import HTTMock # noqa from httmock import response # noqa from httmock import urlmatch # noqa from gitlab import * # noqa @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects/1", method="get") def resp_get_project(url, request): headers = {'content-type': 'application/json'} content = '{"name": "name", "id": 1}'.encode("utf-8") return response(200, content, headers, None, 5, request) @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects", method="get") def resp_list_project(url, request): headers = {'content-type': 'application/json'} content = '[{"name": "name", "id": 1}]'.encode("utf-8") return response(200, content, headers, None, 5, request) @urlmatch(scheme="http", netloc="localhost", path="/api/v4/issues/1", method="get") def resp_get_issue(url, request): headers = {'content-type': 'application/json'} content = '{"name": "name", "id": 1}'.encode("utf-8") return response(200, content, headers, None, 5, request) @urlmatch(scheme="http", netloc="localhost", path="/api/v4/users/1", method="put") def resp_update_user(url, request): headers = {'content-type': 'application/json'} content = ('{"name": "newname", "id": 1, "password": "password", ' '"username": "username", "email": "email"}').encode("utf-8") return response(200, content, headers, None, 5, request) @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects", method="post") def resp_create_project(url, request): headers = {'content-type': 'application/json'} content = '{"name": "testname", "id": 1}'.encode("utf-8") return response(201, content, headers, None, 5, request) @urlmatch(scheme="http", netloc="localhost", path="/api/v4/groups/2/members", method="post") def resp_create_groupmember(url, request): headers = {'content-type': 'application/json'} content = '{"access_level": 50, "id": 3}'.encode("utf-8") return response(201, content, headers, None, 5, request) @urlmatch(scheme="http", netloc="localhost", 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/v4/groups/1", method="delete") def resp_delete_group(url, request): headers = {'content-type': 'application/json'} content = ''.encode("utf-8") return response(200, content, headers, None, 5, request) @urlmatch(scheme="http", netloc="localhost", path="/api/v4/groups/2/projects/3", method="post") def resp_transfer_project(url, request): headers = {'content-type': 'application/json'} content = ''.encode("utf-8") return response(201, content, headers, None, 5, request) @urlmatch(scheme="http", netloc="localhost", path="/api/v4/groups/2/projects/3", method="post") def resp_transfer_project_fail(url, request): headers = {'content-type': 'application/json'} content = '{"message": "messagecontent"}'.encode("utf-8") return response(400, content, headers, None, 5, request) @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects/2/repository/branches/branchname/protect", method="put") def resp_protect_branch(url, request): headers = {'content-type': 'application/json'} content = ''.encode("utf-8") return response(200, content, headers, None, 5, request) @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects/2/repository/branches/branchname/unprotect", method="put") def resp_unprotect_branch(url, request): headers = {'content-type': 'application/json'} content = ''.encode("utf-8") return response(200, content, headers, None, 5, request) @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects/2/repository/branches/branchname/protect", method="put") def resp_protect_branch_fail(url, request): headers = {'content-type': 'application/json'} content = '{"message": "messagecontent"}'.encode("utf-8") return response(400, content, headers, None, 5, request) class TestGitlabObject(unittest.TestCase): def setUp(self): self.gl = Gitlab("http://localhost", private_token="private_token", email="testuser@test.com", password="testpassword", ssl_verify=True) def test_json(self): gl_object = CurrentUser(self.gl, data={"username": "testname"}) json_str = gl_object.json() data = json.loads(json_str) self.assertIn("id", data) self.assertEqual(data["username"], "testname") self.assertEqual(data["gitlab"]["url"], "http://localhost/api/v4") def test_pickability(self): gl_object = CurrentUser(self.gl, data={"username": "testname"}) original_obj_module = gl_object._module pickled = pickle.dumps(gl_object) unpickled = pickle.loads(pickled) self.assertIsInstance(unpickled, CurrentUser) self.assertTrue(hasattr(unpickled, '_module')) self.assertEqual(unpickled._module, original_obj_module) def test_data_for_gitlab(self): class FakeObj1(GitlabObject): _url = '/fake1' requiredCreateAttrs = ['create_req'] optionalCreateAttrs = ['create_opt'] requiredUpdateAttrs = ['update_req'] optionalUpdateAttrs = ['update_opt'] class FakeObj2(GitlabObject): _url = '/fake2' requiredCreateAttrs = ['create_req'] optionalCreateAttrs = ['create_opt'] obj1 = FakeObj1(self.gl, {'update_req': 1, 'update_opt': 1, 'create_req': 1, 'create_opt': 1}) obj2 = FakeObj2(self.gl, {'create_req': 1, 'create_opt': 1}) obj1_data = json.loads(obj1._data_for_gitlab()) self.assertIn('create_req', obj1_data) self.assertIn('create_opt', obj1_data) self.assertNotIn('update_req', obj1_data) self.assertNotIn('update_opt', obj1_data) self.assertNotIn('gitlab', obj1_data) obj1_data = json.loads(obj1._data_for_gitlab(update=True)) self.assertNotIn('create_req', obj1_data) self.assertNotIn('create_opt', obj1_data) self.assertIn('update_req', obj1_data) self.assertIn('update_opt', obj1_data) obj1_data = json.loads(obj1._data_for_gitlab( extra_parameters={'foo': 'bar'})) self.assertIn('foo', obj1_data) self.assertEqual(obj1_data['foo'], 'bar') obj2_data = json.loads(obj2._data_for_gitlab(update=True)) self.assertIn('create_req', obj2_data) self.assertIn('create_opt', obj2_data) def test_list_not_implemented(self): self.assertRaises(NotImplementedError, CurrentUser.list, self.gl) def test_list(self): with HTTMock(resp_list_project): data = Project.list(self.gl, id=1) self.assertEqual(type(data), list) self.assertEqual(len(data), 1) self.assertEqual(type(data[0]), Project) self.assertEqual(data[0].name, "name") self.assertEqual(data[0].id, 1) def test_create_cantcreate(self): gl_object = CurrentUser(self.gl, data={"username": "testname"}) self.assertRaises(NotImplementedError, gl_object._create) def test_create(self): obj = Project(self.gl, data={"name": "testname"}) with HTTMock(resp_create_project): obj._create() self.assertEqual(obj.id, 1) def test_create_with_kw(self): obj = GroupMember(self.gl, data={"access_level": 50, "user_id": 3}, group_id=2) with HTTMock(resp_create_groupmember): obj._create() self.assertEqual(obj.id, 3) self.assertEqual(obj.group_id, 2) self.assertEqual(obj.user_id, 3) self.assertEqual(obj.access_level, 50) def test_get_with_kw(self): with HTTMock(resp_get_projectsnippet): obj = ProjectSnippet(self.gl, data=3, project_id=2) self.assertEqual(obj.id, 3) self.assertEqual(obj.project_id, 2) self.assertEqual(obj.title, "test") def test_create_cantupdate(self): gl_object = CurrentUser(self.gl, data={"username": "testname"}) self.assertRaises(NotImplementedError, gl_object._update) def test_update(self): obj = User(self.gl, data={"name": "testname", "email": "email", "password": "password", "id": 1, "username": "username"}) self.assertEqual(obj.name, "testname") obj.name = "newname" with HTTMock(resp_update_user): obj._update() self.assertEqual(obj.name, "newname") def test_save_with_id(self): obj = User(self.gl, data={"name": "testname", "email": "email", "password": "password", "id": 1, "username": "username"}) self.assertEqual(obj.name, "testname") obj._from_api = True obj.name = "newname" with HTTMock(resp_update_user): obj.save() self.assertEqual(obj.name, "newname") def test_save_without_id(self): obj = Project(self.gl, data={"name": "testname"}) with HTTMock(resp_create_project): obj.save() self.assertEqual(obj.id, 1) def test_delete(self): obj = Group(self.gl, data={"name": "testname", "id": 1}) obj._from_api = True with HTTMock(resp_delete_group): data = obj.delete() self.assertIs(data, True) def test_delete_with_no_id(self): obj = Group(self.gl, data={"name": "testname"}) self.assertRaises(GitlabDeleteError, obj.delete) def test_delete_cant_delete(self): obj = CurrentUser(self.gl, data={"name": "testname", "id": 1}) self.assertRaises(NotImplementedError, obj.delete) def test_set_from_dict_BooleanTrue(self): obj = Project(self.gl, data={"name": "testname"}) data = {"issues_enabled": True} obj._set_from_dict(data) self.assertIs(obj.issues_enabled, True) def test_set_from_dict_BooleanFalse(self): obj = Project(self.gl, data={"name": "testname"}) data = {"issues_enabled": False} obj._set_from_dict(data) self.assertIs(obj.issues_enabled, False) def test_set_from_dict_None(self): obj = Project(self.gl, data={"name": "testname"}) data = {"issues_enabled": None} obj._set_from_dict(data) self.assertIsNone(obj.issues_enabled) class TestGroup(unittest.TestCase): def setUp(self): self.gl = Gitlab("http://localhost", private_token="private_token", email="testuser@test.com", password="testpassword", ssl_verify=True) def test_transfer_project(self): obj = Group(self.gl, data={"name": "testname", "path": "testpath", "id": 2}) with HTTMock(resp_transfer_project): obj.transfer_project(3) def test_transfer_project_fail(self): obj = Group(self.gl, data={"name": "testname", "path": "testpath", "id": 2}) with HTTMock(resp_transfer_project_fail): self.assertRaises(GitlabTransferProjectError, obj.transfer_project, 3) class TestProjectBranch(unittest.TestCase): def setUp(self): self.gl = Gitlab("http://localhost", private_token="private_token", email="testuser@test.com", password="testpassword", ssl_verify=True) self.obj = ProjectBranch(self.gl, data={"name": "branchname", "ref": "ref_name", "id": 3, "project_id": 2}) def test_protect(self): self.assertRaises(AttributeError, getattr, self.obj, 'protected') with HTTMock(resp_protect_branch): self.obj.protect(True) self.assertIs(self.obj.protected, True) def test_protect_unprotect(self): self.obj.protected = True with HTTMock(resp_unprotect_branch): self.obj.protect(False) self.assertRaises(AttributeError, getattr, self.obj, 'protected') def test_protect_unprotect_again(self): self.assertRaises(AttributeError, getattr, self.obj, 'protected') with HTTMock(resp_protect_branch): self.obj.protect(True) self.assertIs(self.obj.protected, True) self.assertEqual(True, self.obj.protected) with HTTMock(resp_unprotect_branch): self.obj.protect(False) self.assertRaises(AttributeError, getattr, self.obj, 'protected') def test_protect_protect_fail(self): with HTTMock(resp_protect_branch_fail): self.assertRaises(GitlabProtectError, self.obj.protect) def test_unprotect(self): self.obj.protected = True with HTTMock(resp_unprotect_branch): self.obj.unprotect() self.assertRaises(AttributeError, getattr, self.obj, 'protected') class TestProjectCommit(unittest.TestCase): def setUp(self): self.gl = Gitlab("http://localhost", private_token="private_token", email="testuser@test.com", password="testpassword", ssl_verify=True) self.obj = ProjectCommit(self.gl, data={"id": 3, "project_id": 2}) @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects/2/repository/commits/3/diff", method="get") def resp_diff(self, url, request): headers = {'content-type': 'application/json'} content = '{"json": 2 }'.encode("utf-8") return response(200, content, headers, None, 5, request) @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects/2/repository/commits/3/diff", method="get") def resp_diff_fail(self, url, request): headers = {'content-type': 'application/json'} content = '{"message": "messagecontent" }'.encode("utf-8") return response(400, content, headers, None, 5, request) @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects/2/repository/blobs/3", method="get") def resp_blob(self, url, request): headers = {'content-type': 'application/json'} content = 'blob'.encode("utf-8") return response(200, content, headers, None, 5, request) @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects/2/repository/blobs/3", method="get") def resp_blob_fail(self, url, request): headers = {'content-type': 'application/json'} content = '{"message": "messagecontent" }'.encode("utf-8") return response(400, content, headers, None, 5, request) def test_diff(self): with HTTMock(self.resp_diff): data = {"json": 2} diff = self.obj.diff() self.assertEqual(diff, data) def test_diff_fail(self): with HTTMock(self.resp_diff_fail): self.assertRaises(GitlabGetError, self.obj.diff) def test_blob(self): with HTTMock(self.resp_blob): blob = self.obj.blob("testing") self.assertEqual(blob, b'blob') def test_blob_fail(self): with HTTMock(self.resp_blob_fail): self.assertRaises(GitlabGetError, self.obj.blob, "testing") class TestProjectSnippet(unittest.TestCase): def setUp(self): self.gl = Gitlab("http://localhost", private_token="private_token", email="testuser@test.com", password="testpassword", ssl_verify=True) self.obj = ProjectSnippet(self.gl, data={"id": 3, "project_id": 2}) @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects/2/snippets/3/raw", method="get") def resp_content(self, url, request): headers = {'content-type': 'application/json'} content = 'content'.encode("utf-8") return response(200, content, headers, None, 5, request) @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects/2/snippets/3/raw", method="get") def resp_content_fail(self, url, request): headers = {'content-type': 'application/json'} content = '{"message": "messagecontent" }'.encode("utf-8") return response(400, content, headers, None, 5, request) def test_content(self): with HTTMock(self.resp_content): data = b'content' content = self.obj.content() self.assertEqual(content, data) def test_blob_fail(self): with HTTMock(self.resp_content_fail): self.assertRaises(GitlabGetError, self.obj.content) class TestSnippet(unittest.TestCase): def setUp(self): self.gl = Gitlab("http://localhost", private_token="private_token", email="testuser@test.com", password="testpassword", ssl_verify=True) self.obj = Snippet(self.gl, data={"id": 3}) @urlmatch(scheme="http", netloc="localhost", path="/api/v4/snippets/3/raw", method="get") def resp_content(self, url, request): headers = {'content-type': 'application/json'} content = 'content'.encode("utf-8") return response(200, content, headers, None, 5, request) @urlmatch(scheme="http", netloc="localhost", path="/api/v4/snippets/3/raw", method="get") def resp_content_fail(self, url, request): headers = {'content-type': 'application/json'} content = '{"message": "messagecontent" }'.encode("utf-8") return response(400, content, headers, None, 5, request) def test_content(self): with HTTMock(self.resp_content): data = b'content' content = self.obj.raw() self.assertEqual(content, data) def test_blob_fail(self): with HTTMock(self.resp_content_fail): self.assertRaises(GitlabGetError, self.obj.raw) python-gitlab-1.3.0/gitlab/tests/test_manager.py000066400000000000000000000307121324224150200217210ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Copyright (C) 2016-2017 Gauvain Pocentek # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . try: import unittest except ImportError: import unittest2 as unittest from httmock import HTTMock # noqa from httmock import response # noqa from httmock import urlmatch # noqa from gitlab import * # noqa from gitlab.v3.objects import BaseManager # noqa class FakeChildObject(GitlabObject): _url = "/fake/%(parent_id)s/fakechild" requiredCreateAttrs = ['name'] requiredUrlAttrs = ['parent_id'] class FakeChildManager(BaseManager): obj_cls = FakeChildObject class FakeObject(GitlabObject): _url = "/fake" requiredCreateAttrs = ['name'] managers = [('children', FakeChildManager, [('parent_id', 'id')])] class FakeObjectManager(BaseManager): obj_cls = FakeObject 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, api_version=3) def test_set_parent_args(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v3/fake", method="POST") def resp_create(url, request): headers = {'content-type': 'application/json'} content = '{"id": 1, "name": "name"}'.encode("utf-8") return response(201, content, headers, None, 5, request) mgr = FakeChildManager(self.gitlab) args = mgr._set_parent_args(name="name") self.assertEqual(args, {"name": "name"}) with HTTMock(resp_create): o = FakeObjectManager(self.gitlab).create({"name": "name"}) args = o.children._set_parent_args(name="name") self.assertEqual(args, {"name": "name", "parent_id": 1}) def test_constructor(self): self.assertRaises(AttributeError, BaseManager, self.gitlab) @urlmatch(scheme="http", netloc="localhost", path="/api/v3/fake/1", method="get") def resp_get(url, request): headers = {'content-type': 'application/json'} content = '{"id": 1, "name": "fake_name"}'.encode("utf-8") return response(200, content, headers, None, 5, request) with HTTMock(resp_get): mgr = FakeObjectManager(self.gitlab) fake_obj = mgr.get(1) self.assertEqual(fake_obj.id, 1) self.assertEqual(fake_obj.name, "fake_name") self.assertEqual(mgr.gitlab, self.gitlab) self.assertEqual(mgr.args, []) self.assertEqual(mgr.parent, None) self.assertIsInstance(fake_obj.children, FakeChildManager) self.assertEqual(fake_obj.children.gitlab, self.gitlab) self.assertEqual(fake_obj.children.parent, fake_obj) self.assertEqual(len(fake_obj.children.args), 1) fake_child = fake_obj.children.get(1) self.assertEqual(fake_child.id, 1) self.assertEqual(fake_child.name, "fake_name") def test_get(self): mgr = FakeObjectManager(self.gitlab) FakeObject.canGet = False self.assertRaises(NotImplementedError, mgr.get, 1) @urlmatch(scheme="http", netloc="localhost", path="/api/v3/fake/1", method="get") def resp_get(url, request): headers = {'content-type': 'application/json'} content = '{"id": 1, "name": "fake_name"}'.encode("utf-8") return response(200, content, headers, None, 5, request) with HTTMock(resp_get): FakeObject.canGet = True mgr = FakeObjectManager(self.gitlab) fake_obj = mgr.get(1) self.assertIsInstance(fake_obj, FakeObject) self.assertEqual(fake_obj.id, 1) self.assertEqual(fake_obj.name, "fake_name") def test_list(self): mgr = FakeObjectManager(self.gitlab) FakeObject.canList = False self.assertRaises(NotImplementedError, mgr.list) @urlmatch(scheme="http", netloc="localhost", path="/api/v3/fake", method="get") def resp_get(url, request): headers = {'content-type': 'application/json'} content = ('[{"id": 1, "name": "fake_name1"},' '{"id": 2, "name": "fake_name2"}]') content = content.encode("utf-8") return response(200, content, headers, None, 5, request) with HTTMock(resp_get): FakeObject.canList = True mgr = FakeObjectManager(self.gitlab) fake_list = mgr.list() self.assertEqual(len(fake_list), 2) self.assertIsInstance(fake_list[0], FakeObject) self.assertEqual(fake_list[0].id, 1) self.assertEqual(fake_list[0].name, "fake_name1") self.assertIsInstance(fake_list[1], FakeObject) self.assertEqual(fake_list[1].id, 2) self.assertEqual(fake_list[1].name, "fake_name2") def test_create(self): mgr = FakeObjectManager(self.gitlab) FakeObject.canCreate = False self.assertRaises(NotImplementedError, mgr.create, {'name': 'name'}) @urlmatch(scheme="http", netloc="localhost", path="/api/v3/fake", method="post") def resp_post(url, request): headers = {'content-type': 'application/json'} data = '{"name": "fake_name"}' content = '{"id": 1, "name": "fake_name"}'.encode("utf-8") return response(201, content, headers, data, 5, request) with HTTMock(resp_post): FakeObject.canCreate = True mgr = FakeObjectManager(self.gitlab) fake_obj = mgr.create({'name': 'fake_name'}) self.assertIsInstance(fake_obj, FakeObject) self.assertEqual(fake_obj.id, 1) self.assertEqual(fake_obj.name, "fake_name") def test_project_manager_owned(self): mgr = ProjectManager(self.gitlab) @urlmatch(scheme="http", netloc="localhost", path="/api/v3/projects/owned", method="get") def resp_get_all(url, request): headers = {'content-type': 'application/json'} content = ('[{"name": "name1", "id": 1}, ' '{"name": "name2", "id": 2}]') content = content.encode("utf-8") return response(200, content, headers, None, 5, request) with HTTMock(resp_get_all): data = mgr.owned() self.assertEqual(type(data), list) self.assertEqual(2, len(data)) self.assertEqual(type(data[0]), Project) self.assertEqual(type(data[1]), Project) self.assertEqual(data[0].name, "name1") self.assertEqual(data[1].name, "name2") self.assertEqual(data[0].id, 1) self.assertEqual(data[1].id, 2) def test_project_manager_all(self): mgr = ProjectManager(self.gitlab) @urlmatch(scheme="http", netloc="localhost", path="/api/v3/projects/all", method="get") def resp_get_all(url, request): headers = {'content-type': 'application/json'} content = ('[{"name": "name1", "id": 1}, ' '{"name": "name2", "id": 2}]') content = content.encode("utf-8") return response(200, content, headers, None, 5, request) with HTTMock(resp_get_all): data = mgr.all() self.assertEqual(type(data), list) self.assertEqual(2, len(data)) self.assertEqual(type(data[0]), Project) self.assertEqual(type(data[1]), Project) self.assertEqual(data[0].name, "name1") self.assertEqual(data[1].name, "name2") self.assertEqual(data[0].id, 1) self.assertEqual(data[1].id, 2) def test_project_manager_search(self): mgr = ProjectManager(self.gitlab) @urlmatch(scheme="http", netloc="localhost", path="/api/v3/projects", query="search=foo", method="get") def resp_get_all(url, request): headers = {'content-type': 'application/json'} content = ('[{"name": "foo1", "id": 1}, ' '{"name": "foo2", "id": 2}]') content = content.encode("utf-8") return response(200, content, headers, None, 5, request) with HTTMock(resp_get_all): data = mgr.list(search='foo') self.assertEqual(type(data), list) self.assertEqual(2, len(data)) self.assertEqual(type(data[0]), Project) self.assertEqual(type(data[1]), Project) self.assertEqual(data[0].name, "foo1") self.assertEqual(data[1].name, "foo2") self.assertEqual(data[0].id, 1) self.assertEqual(data[1].id, 2) def test_user_manager_search(self): mgr = UserManager(self.gitlab) @urlmatch(scheme="http", netloc="localhost", path="/api/v3/users", query="search=foo", method="get") def resp_get_search(url, request): headers = {'content-type': 'application/json'} content = ('[{"name": "foo1", "id": 1}, ' '{"name": "foo2", "id": 2}]') content = content.encode("utf-8") return response(200, content, headers, None, 5, request) with HTTMock(resp_get_search): data = mgr.search('foo') self.assertEqual(type(data), list) self.assertEqual(2, len(data)) self.assertEqual(type(data[0]), User) self.assertEqual(type(data[1]), User) self.assertEqual(data[0].name, "foo1") self.assertEqual(data[1].name, "foo2") self.assertEqual(data[0].id, 1) self.assertEqual(data[1].id, 2) def test_user_manager_get_by_username(self): mgr = UserManager(self.gitlab) @urlmatch(scheme="http", netloc="localhost", path="/api/v3/users", query="username=foo", method="get") def resp_get_username(url, request): headers = {'content-type': 'application/json'} content = '[{"name": "foo", "id": 1}]'.encode("utf-8") return response(200, content, headers, None, 5, request) with HTTMock(resp_get_username): data = mgr.get_by_username('foo') self.assertEqual(type(data), User) self.assertEqual(data.name, "foo") self.assertEqual(data.id, 1) @urlmatch(scheme="http", netloc="localhost", path="/api/v3/users", query="username=foo", method="get") def resp_get_username_nomatch(url, request): headers = {'content-type': 'application/json'} content = '[]'.encode("utf-8") return response(200, content, headers, None, 5, request) with HTTMock(resp_get_username_nomatch): self.assertRaises(GitlabGetError, mgr.get_by_username, 'foo') def test_group_manager_search(self): mgr = GroupManager(self.gitlab) @urlmatch(scheme="http", netloc="localhost", path="/api/v3/groups", query="search=foo", method="get") def resp_get_search(url, request): headers = {'content-type': 'application/json'} content = ('[{"name": "foo1", "id": 1}, ' '{"name": "foo2", "id": 2}]') content = content.encode("utf-8") return response(200, content, headers, None, 5, request) with HTTMock(resp_get_search): data = mgr.search('foo') self.assertEqual(type(data), list) self.assertEqual(2, len(data)) self.assertEqual(type(data[0]), Group) self.assertEqual(type(data[1]), Group) self.assertEqual(data[0].name, "foo1") self.assertEqual(data[1].name, "foo2") self.assertEqual(data[0].id, 1) self.assertEqual(data[1].id, 2) python-gitlab-1.3.0/gitlab/tests/test_mixins.py000066400000000000000000000356771324224150200216350ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Copyright (C) 2014 Mika Mäenpää , # Tampere University of Technology # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . from __future__ import print_function try: import unittest except ImportError: import unittest2 as unittest from httmock import HTTMock # noqa from httmock import response # noqa from httmock import urlmatch # noqa from gitlab import * # noqa from gitlab.base import * # noqa from gitlab.mixins import * # noqa class TestObjectMixinsAttributes(unittest.TestCase): def test_access_request_mixin(self): class O(AccessRequestMixin): pass obj = O() self.assertTrue(hasattr(obj, 'approve')) def test_subscribable_mixin(self): class O(SubscribableMixin): pass obj = O() self.assertTrue(hasattr(obj, 'subscribe')) self.assertTrue(hasattr(obj, 'unsubscribe')) def test_todo_mixin(self): class O(TodoMixin): pass obj = O() self.assertTrue(hasattr(obj, 'todo')) def test_time_tracking_mixin(self): class O(TimeTrackingMixin): pass obj = O() self.assertTrue(hasattr(obj, 'time_stats')) self.assertTrue(hasattr(obj, 'time_estimate')) self.assertTrue(hasattr(obj, 'reset_time_estimate')) self.assertTrue(hasattr(obj, 'add_spent_time')) self.assertTrue(hasattr(obj, 'reset_spent_time')) def test_set_mixin(self): class O(SetMixin): pass obj = O() self.assertTrue(hasattr(obj, 'set')) class TestMetaMixins(unittest.TestCase): def test_retrieve_mixin(self): class M(RetrieveMixin): pass obj = M() self.assertTrue(hasattr(obj, 'list')) self.assertTrue(hasattr(obj, 'get')) self.assertFalse(hasattr(obj, 'create')) self.assertFalse(hasattr(obj, 'update')) self.assertFalse(hasattr(obj, 'delete')) self.assertIsInstance(obj, ListMixin) self.assertIsInstance(obj, GetMixin) def test_crud_mixin(self): class M(CRUDMixin): pass obj = M() self.assertTrue(hasattr(obj, 'get')) self.assertTrue(hasattr(obj, 'list')) self.assertTrue(hasattr(obj, 'create')) self.assertTrue(hasattr(obj, 'update')) self.assertTrue(hasattr(obj, 'delete')) self.assertIsInstance(obj, ListMixin) self.assertIsInstance(obj, GetMixin) self.assertIsInstance(obj, CreateMixin) self.assertIsInstance(obj, UpdateMixin) self.assertIsInstance(obj, DeleteMixin) def test_no_update_mixin(self): class M(NoUpdateMixin): pass obj = M() self.assertTrue(hasattr(obj, 'get')) self.assertTrue(hasattr(obj, 'list')) self.assertTrue(hasattr(obj, 'create')) self.assertFalse(hasattr(obj, 'update')) self.assertTrue(hasattr(obj, 'delete')) self.assertIsInstance(obj, ListMixin) self.assertIsInstance(obj, GetMixin) self.assertIsInstance(obj, CreateMixin) self.assertNotIsInstance(obj, UpdateMixin) self.assertIsInstance(obj, DeleteMixin) class FakeObject(base.RESTObject): pass class FakeManager(base.RESTManager): _path = '/tests' _obj_cls = FakeObject class TestMixinMethods(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 @urlmatch(scheme="http", netloc="localhost", path='/api/v4/tests/42', method="get") def resp_cont(url, request): headers = {'Content-Type': 'application/json'} content = '{"id": 42, "foo": "bar"}' return response(200, content, headers, None, 5, request) with HTTMock(resp_cont): mgr = M(self.gl) obj = mgr.get(42) self.assertIsInstance(obj, FakeObject) self.assertEqual(obj.foo, 'bar') self.assertEqual(obj.id, 42) def test_get_without_id_mixin(self): class M(GetWithoutIdMixin, FakeManager): pass @urlmatch(scheme="http", netloc="localhost", path='/api/v4/tests', method="get") def resp_cont(url, request): headers = {'Content-Type': 'application/json'} content = '{"foo": "bar"}' return response(200, content, headers, None, 5, request) with HTTMock(resp_cont): mgr = M(self.gl) obj = mgr.get() self.assertIsInstance(obj, FakeObject) self.assertEqual(obj.foo, 'bar') self.assertFalse(hasattr(obj, 'id')) def test_list_mixin(self): class M(ListMixin, FakeManager): pass @urlmatch(scheme="http", netloc="localhost", path='/api/v4/tests', method="get") def resp_cont(url, request): headers = {'Content-Type': 'application/json'} content = '[{"id": 42, "foo": "bar"},{"id": 43, "foo": "baz"}]' return response(200, content, headers, None, 5, request) with HTTMock(resp_cont): # test RESTObjectList mgr = M(self.gl) obj_list = mgr.list(as_list=False) self.assertIsInstance(obj_list, base.RESTObjectList) for obj in obj_list: self.assertIsInstance(obj, FakeObject) self.assertIn(obj.id, (42, 43)) # test list() obj_list = mgr.list(all=True) self.assertIsInstance(obj_list, list) self.assertEqual(obj_list[0].id, 42) self.assertEqual(obj_list[1].id, 43) self.assertIsInstance(obj_list[0], FakeObject) self.assertEqual(len(obj_list), 2) def test_list_other_url(self): class M(ListMixin, FakeManager): pass @urlmatch(scheme="http", netloc="localhost", path='/api/v4/others', method="get") def resp_cont(url, request): headers = {'Content-Type': 'application/json'} content = '[{"id": 42, "foo": "bar"}]' return response(200, content, headers, None, 5, request) with HTTMock(resp_cont): mgr = M(self.gl) obj_list = mgr.list(path='/others', as_list=False) self.assertIsInstance(obj_list, base.RESTObjectList) obj = obj_list.next() self.assertEqual(obj.id, 42) self.assertEqual(obj.foo, 'bar') self.assertRaises(StopIteration, obj_list.next) def test_get_from_list_mixin(self): class M(GetFromListMixin, FakeManager): pass @urlmatch(scheme="http", netloc="localhost", path='/api/v4/tests', method="get") def resp_cont(url, request): headers = {'Content-Type': 'application/json'} content = '[{"id": 42, "foo": "bar"},{"id": 43, "foo": "baz"}]' return response(200, content, headers, None, 5, request) with HTTMock(resp_cont): mgr = M(self.gl) obj = mgr.get(42) self.assertIsInstance(obj, FakeObject) self.assertEqual(obj.foo, 'bar') self.assertEqual(obj.id, 42) self.assertRaises(GitlabGetError, mgr.get, 44) def test_create_mixin_get_attrs(self): class M1(CreateMixin, FakeManager): pass class M2(CreateMixin, FakeManager): _create_attrs = (('foo',), ('bar', 'baz')) _update_attrs = (('foo',), ('bam', )) mgr = M1(self.gl) required, optional = mgr.get_create_attrs() self.assertEqual(len(required), 0) self.assertEqual(len(optional), 0) mgr = M2(self.gl) required, optional = mgr.get_create_attrs() self.assertIn('foo', required) self.assertIn('bar', optional) self.assertIn('baz', optional) self.assertNotIn('bam', optional) def test_create_mixin_missing_attrs(self): class M(CreateMixin, FakeManager): _create_attrs = (('foo',), ('bar', 'baz')) mgr = M(self.gl) data = {'foo': 'bar', 'baz': 'blah'} mgr._check_missing_create_attrs(data) data = {'baz': 'blah'} with self.assertRaises(AttributeError) as error: mgr._check_missing_create_attrs(data) self.assertIn('foo', str(error.exception)) def test_create_mixin(self): class M(CreateMixin, FakeManager): _create_attrs = (('foo',), ('bar', 'baz')) _update_attrs = (('foo',), ('bam', )) @urlmatch(scheme="http", netloc="localhost", path='/api/v4/tests', method="post") def resp_cont(url, request): headers = {'Content-Type': 'application/json'} content = '{"id": 42, "foo": "bar"}' return response(200, content, headers, None, 5, request) with HTTMock(resp_cont): mgr = M(self.gl) obj = mgr.create({'foo': 'bar'}) self.assertIsInstance(obj, FakeObject) self.assertEqual(obj.id, 42) self.assertEqual(obj.foo, 'bar') def test_create_mixin_custom_path(self): class M(CreateMixin, FakeManager): _create_attrs = (('foo',), ('bar', 'baz')) _update_attrs = (('foo',), ('bam', )) @urlmatch(scheme="http", netloc="localhost", path='/api/v4/others', method="post") def resp_cont(url, request): headers = {'Content-Type': 'application/json'} content = '{"id": 42, "foo": "bar"}' return response(200, content, headers, None, 5, request) with HTTMock(resp_cont): mgr = M(self.gl) obj = mgr.create({'foo': 'bar'}, path='/others') self.assertIsInstance(obj, FakeObject) self.assertEqual(obj.id, 42) self.assertEqual(obj.foo, 'bar') def test_update_mixin_get_attrs(self): class M1(UpdateMixin, FakeManager): pass class M2(UpdateMixin, FakeManager): _create_attrs = (('foo',), ('bar', 'baz')) _update_attrs = (('foo',), ('bam', )) mgr = M1(self.gl) required, optional = mgr.get_update_attrs() self.assertEqual(len(required), 0) self.assertEqual(len(optional), 0) mgr = M2(self.gl) required, optional = mgr.get_update_attrs() self.assertIn('foo', required) self.assertIn('bam', optional) self.assertNotIn('bar', optional) self.assertNotIn('baz', optional) def test_update_mixin_missing_attrs(self): class M(UpdateMixin, FakeManager): _update_attrs = (('foo',), ('bar', 'baz')) mgr = M(self.gl) data = {'foo': 'bar', 'baz': 'blah'} mgr._check_missing_update_attrs(data) data = {'baz': 'blah'} with self.assertRaises(AttributeError) as error: mgr._check_missing_update_attrs(data) self.assertIn('foo', str(error.exception)) def test_update_mixin(self): class M(UpdateMixin, FakeManager): _create_attrs = (('foo',), ('bar', 'baz')) _update_attrs = (('foo',), ('bam', )) @urlmatch(scheme="http", netloc="localhost", path='/api/v4/tests/42', method="put") def resp_cont(url, request): headers = {'Content-Type': 'application/json'} content = '{"id": 42, "foo": "baz"}' return response(200, content, headers, None, 5, request) with HTTMock(resp_cont): mgr = M(self.gl) server_data = mgr.update(42, {'foo': 'baz'}) self.assertIsInstance(server_data, dict) self.assertEqual(server_data['id'], 42) self.assertEqual(server_data['foo'], 'baz') def test_update_mixin_no_id(self): class M(UpdateMixin, FakeManager): _create_attrs = (('foo',), ('bar', 'baz')) _update_attrs = (('foo',), ('bam', )) @urlmatch(scheme="http", netloc="localhost", path='/api/v4/tests', method="put") def resp_cont(url, request): headers = {'Content-Type': 'application/json'} content = '{"foo": "baz"}' return response(200, content, headers, None, 5, request) with HTTMock(resp_cont): mgr = M(self.gl) server_data = mgr.update(new_data={'foo': 'baz'}) self.assertIsInstance(server_data, dict) self.assertEqual(server_data['foo'], 'baz') def test_delete_mixin(self): class M(DeleteMixin, FakeManager): pass @urlmatch(scheme="http", netloc="localhost", path='/api/v4/tests/42', method="delete") def resp_cont(url, request): headers = {'Content-Type': 'application/json'} content = '' return response(200, content, headers, None, 5, request) with HTTMock(resp_cont): mgr = M(self.gl) mgr.delete(42) def test_save_mixin(self): class M(UpdateMixin, FakeManager): pass class O(SaveMixin, RESTObject): pass @urlmatch(scheme="http", netloc="localhost", path='/api/v4/tests/42', method="put") def resp_cont(url, request): headers = {'Content-Type': 'application/json'} content = '{"id": 42, "foo": "baz"}' return response(200, content, headers, None, 5, request) with HTTMock(resp_cont): mgr = M(self.gl) obj = O(mgr, {'id': 42, 'foo': 'bar'}) obj.foo = 'baz' obj.save() self.assertEqual(obj._attrs['foo'], 'baz') self.assertDictEqual(obj._updated_attrs, {}) def test_set_mixin(self): class M(SetMixin, FakeManager): pass @urlmatch(scheme="http", netloc="localhost", path='/api/v4/tests/foo', method="put") def resp_cont(url, request): headers = {'Content-Type': 'application/json'} content = '{"key": "foo", "value": "bar"}' return response(200, content, headers, None, 5, request) with HTTMock(resp_cont): mgr = M(self.gl) obj = mgr.set('foo', 'bar') self.assertIsInstance(obj, FakeObject) self.assertEqual(obj.key, 'foo') self.assertEqual(obj.value, 'bar') python-gitlab-1.3.0/gitlab/utils.py000066400000000000000000000021371324224150200172460ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Copyright (C) 2016-2017 Gauvain Pocentek # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . class _StdoutStream(object): def __call__(self, chunk): print(chunk) def response_content(response, streamed, action, chunk_size): if streamed is False: return response.content if action is None: action = _StdoutStream() for chunk in response.iter_content(chunk_size=chunk_size): if chunk: action(chunk) python-gitlab-1.3.0/gitlab/v3/000077500000000000000000000000001324224150200160615ustar00rootroot00000000000000python-gitlab-1.3.0/gitlab/v3/__init__.py000066400000000000000000000000001324224150200201600ustar00rootroot00000000000000python-gitlab-1.3.0/gitlab/v3/cli.py000066400000000000000000000445431324224150200172140ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright (C) 2013-2017 Gauvain Pocentek # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . from __future__ import print_function from __future__ import absolute_import import inspect import operator import sys import six import gitlab import gitlab.base from gitlab import cli import gitlab.v3.objects EXTRA_ACTIONS = { gitlab.v3.objects.Group: { 'search': {'required': ['query']}}, gitlab.v3.objects.ProjectBranch: { 'protect': {'required': ['id', 'project-id']}, 'unprotect': {'required': ['id', 'project-id']}}, gitlab.v3.objects.ProjectBuild: { 'cancel': {'required': ['id', 'project-id']}, 'retry': {'required': ['id', 'project-id']}, 'artifacts': {'required': ['id', 'project-id']}, 'trace': {'required': ['id', 'project-id']}}, gitlab.v3.objects.ProjectCommit: { 'diff': {'required': ['id', 'project-id']}, 'blob': {'required': ['id', 'project-id', 'filepath']}, 'builds': {'required': ['id', 'project-id']}, 'cherrypick': {'required': ['id', 'project-id', 'branch']}}, gitlab.v3.objects.ProjectIssue: { 'subscribe': {'required': ['id', 'project-id']}, 'unsubscribe': {'required': ['id', 'project-id']}, 'move': {'required': ['id', 'project-id', 'to-project-id']}}, gitlab.v3.objects.ProjectMergeRequest: { 'closes-issues': {'required': ['id', 'project-id']}, 'cancel': {'required': ['id', 'project-id']}, 'merge': {'required': ['id', 'project-id'], 'optional': ['merge-commit-message', 'should-remove-source-branch', 'merged-when-build-succeeds']}}, gitlab.v3.objects.ProjectMilestone: { 'issues': {'required': ['id', 'project-id']}}, gitlab.v3.objects.Project: { 'search': {'required': ['query']}, 'owned': {}, 'all': {'optional': [('all', bool)]}, 'starred': {}, 'star': {'required': ['id']}, 'unstar': {'required': ['id']}, 'archive': {'required': ['id']}, 'unarchive': {'required': ['id']}, 'share': {'required': ['id', 'group-id', 'group-access']}, 'upload': {'required': ['id', 'filename', 'filepath']}}, gitlab.v3.objects.User: { 'block': {'required': ['id']}, 'unblock': {'required': ['id']}, 'search': {'required': ['query']}, 'get-by-username': {'required': ['query']}}, } class GitlabCLI(object): def _get_id(self, cls, args): try: id = args.pop(cls.idAttr) except Exception: cli.die("Missing --%s argument" % cls.idAttr.replace('_', '-')) return id def do_create(self, cls, gl, what, args): if not cls.canCreate: cli.die("%s objects can't be created" % what) try: o = cls.create(gl, args) except Exception as e: cli.die("Impossible to create object", e) return o def do_list(self, cls, gl, what, args): if not cls.canList: cli.die("%s objects can't be listed" % what) try: l = cls.list(gl, **args) except Exception as e: cli.die("Impossible to list objects", e) return l def do_get(self, cls, gl, what, args): if cls.canGet is False: cli.die("%s objects can't be retrieved" % what) id = None if cls not in [gitlab.v3.objects.CurrentUser] and cls.getRequiresId: id = self._get_id(cls, args) try: o = cls.get(gl, id, **args) except Exception as e: cli.die("Impossible to get object", e) return o def do_delete(self, cls, gl, what, args): if not cls.canDelete: cli.die("%s objects can't be deleted" % what) id = args.pop(cls.idAttr) try: gl.delete(cls, id, **args) except Exception as e: cli.die("Impossible to destroy object", e) def do_update(self, cls, gl, what, args): if not cls.canUpdate: cli.die("%s objects can't be updated" % what) o = self.do_get(cls, gl, what, args) try: for k, v in args.items(): o.__dict__[k] = v o.save() except Exception as e: cli.die("Impossible to update object", e) return o def do_group_search(self, cls, gl, what, args): try: return gl.groups.search(args['query']) except Exception as e: cli.die("Impossible to search projects", e) def do_project_search(self, cls, gl, what, args): try: return gl.projects.search(args['query']) except Exception as e: cli.die("Impossible to search projects", e) def do_project_all(self, cls, gl, what, args): try: return gl.projects.all(all=args.get('all', False)) except Exception as e: cli.die("Impossible to list all projects", e) def do_project_starred(self, cls, gl, what, args): try: return gl.projects.starred() except Exception as e: cli.die("Impossible to list starred projects", e) def do_project_owned(self, cls, gl, what, args): try: return gl.projects.owned() except Exception as e: cli.die("Impossible to list owned projects", e) def do_project_star(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) o.star() except Exception as e: cli.die("Impossible to star project", e) def do_project_unstar(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) o.unstar() except Exception as e: cli.die("Impossible to unstar project", e) def do_project_archive(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) o.archive_() except Exception as e: cli.die("Impossible to archive project", e) def do_project_unarchive(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) o.unarchive_() except Exception as e: cli.die("Impossible to unarchive project", e) def do_project_share(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) o.share(args['group_id'], args['group_access']) except Exception as e: cli.die("Impossible to share project", e) def do_user_block(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) o.block() except Exception as e: cli.die("Impossible to block user", e) def do_user_unblock(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) o.unblock() except Exception as e: cli.die("Impossible to block user", e) def do_project_commit_diff(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) return [x['diff'] for x in o.diff()] except Exception as e: cli.die("Impossible to get commit diff", e) def do_project_commit_blob(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) return o.blob(args['filepath']) except Exception as e: cli.die("Impossible to get commit blob", e) def do_project_commit_builds(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) return o.builds() except Exception as e: cli.die("Impossible to get commit builds", e) def do_project_commit_cherrypick(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) o.cherry_pick(branch=args['branch']) except Exception as e: cli.die("Impossible to cherry-pick commit", e) def do_project_build_cancel(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) return o.cancel() except Exception as e: cli.die("Impossible to cancel project build", e) def do_project_build_retry(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) return o.retry() except Exception as e: cli.die("Impossible to retry project build", e) def do_project_build_artifacts(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) return o.artifacts() except Exception as e: cli.die("Impossible to get project build artifacts", e) def do_project_build_trace(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) return o.trace() except Exception as e: cli.die("Impossible to get project build trace", e) def do_project_issue_subscribe(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) o.subscribe() except Exception as e: cli.die("Impossible to subscribe to issue", e) def do_project_issue_unsubscribe(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) o.unsubscribe() except Exception as e: cli.die("Impossible to subscribe to issue", e) def do_project_issue_move(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) o.move(args['to_project_id']) except Exception as e: cli.die("Impossible to move issue", e) def do_project_merge_request_closesissues(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) return o.closes_issues() except Exception as e: cli.die("Impossible to list issues closed by merge request", e) def do_project_merge_request_cancel(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) return o.cancel_merge_when_build_succeeds() except Exception as e: cli.die("Impossible to cancel merge request", e) def do_project_merge_request_merge(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) should_remove = args.get('should_remove_source_branch', False) build_succeeds = args.get('merged_when_build_succeeds', False) return o.merge( merge_commit_message=args.get('merge_commit_message', ''), should_remove_source_branch=should_remove, merged_when_build_succeeds=build_succeeds) except Exception as e: cli.die("Impossible to validate merge request", e) def do_project_milestone_issues(self, cls, gl, what, args): try: o = self.do_get(cls, gl, what, args) return o.issues() except Exception as e: cli.die("Impossible to get milestone issues", e) def do_user_search(self, cls, gl, what, args): try: return gl.users.search(args['query']) except Exception as e: cli.die("Impossible to search users", e) def do_user_getbyusername(self, cls, gl, what, args): try: return gl.users.search(args['query']) except Exception as e: cli.die("Impossible to get user %s" % args['query'], e) def do_project_upload(self, cls, gl, what, args): try: project = gl.projects.get(args["id"]) except Exception as e: cli.die("Could not load project '{!r}'".format(args["id"]), e) try: res = project.upload(filename=args["filename"], filepath=args["filepath"]) except Exception as e: cli.die("Could not upload file into project", e) return res def _populate_sub_parser_by_class(cls, sub_parser): for action_name in ['list', 'get', 'create', 'update', 'delete']: attr = 'can' + action_name.capitalize() if not getattr(cls, attr): continue sub_parser_action = sub_parser.add_parser(action_name) [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), required=True) for x in cls.requiredUrlAttrs] sub_parser_action.add_argument("--sudo", required=False) if action_name == "list": [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), required=True) for x in cls.requiredListAttrs] sub_parser_action.add_argument("--page", required=False) sub_parser_action.add_argument("--per-page", required=False) sub_parser_action.add_argument("--all", required=False, action='store_true') if action_name in ["get", "delete"]: if cls not in [gitlab.v3.objects.CurrentUser]: if cls.getRequiresId: id_attr = cls.idAttr.replace('_', '-') sub_parser_action.add_argument("--%s" % id_attr, required=True) [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), required=True) for x in cls.requiredGetAttrs if x != cls.idAttr] if action_name == "get": [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), required=False) for x in cls.optionalGetAttrs] if action_name == "list": [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), required=False) for x in cls.optionalListAttrs] if action_name == "create": [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), required=True) for x in cls.requiredCreateAttrs] [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), required=False) for x in cls.optionalCreateAttrs] if action_name == "update": id_attr = cls.idAttr.replace('_', '-') sub_parser_action.add_argument("--%s" % id_attr, required=True) attrs = (cls.requiredUpdateAttrs if (cls.requiredUpdateAttrs or cls.optionalUpdateAttrs) else cls.requiredCreateAttrs) [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), required=True) for x in attrs if x != cls.idAttr] attrs = (cls.optionalUpdateAttrs if (cls.requiredUpdateAttrs or cls.optionalUpdateAttrs) else cls.optionalCreateAttrs) [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), required=False) for x in attrs] if cls in EXTRA_ACTIONS: def _add_arg(parser, required, data): extra_args = {} if isinstance(data, tuple): if data[1] is bool: extra_args = {'action': 'store_true'} data = data[0] parser.add_argument("--%s" % data, required=required, **extra_args) for action_name in sorted(EXTRA_ACTIONS[cls]): sub_parser_action = sub_parser.add_parser(action_name) d = EXTRA_ACTIONS[cls][action_name] [_add_arg(sub_parser_action, True, arg) for arg in d.get('required', [])] [_add_arg(sub_parser_action, False, arg) for arg in d.get('optional', [])] def extend_parser(parser): subparsers = parser.add_subparsers(title='object', dest='what', help="Object to manipulate.") subparsers.required = True # populate argparse for all Gitlab Object classes = [] for cls in gitlab.v3.objects.__dict__.values(): try: if gitlab.base.GitlabObject in inspect.getmro(cls): classes.append(cls) except AttributeError: pass classes.sort(key=operator.attrgetter("__name__")) for cls in classes: arg_name = cli.cls_to_what(cls) object_group = subparsers.add_parser(arg_name) object_subparsers = object_group.add_subparsers( dest='action', help="Action to execute.") _populate_sub_parser_by_class(cls, object_subparsers) object_subparsers.required = True return parser def run(gl, what, action, args, verbose, *fargs, **kwargs): try: cls = gitlab.v3.objects.__dict__[cli.what_to_cls(what)] except ImportError: cli.die("Unknown object: %s" % what) g_cli = GitlabCLI() method = None what = what.replace('-', '_') action = action.lower().replace('-', '') for test in ["do_%s_%s" % (what, action), "do_%s" % action]: if hasattr(g_cli, test): method = test break if method is None: sys.stderr.write("Don't know how to deal with this!\n") sys.exit(1) ret_val = getattr(g_cli, method)(cls, gl, what, args) if isinstance(ret_val, list): for o in ret_val: if isinstance(o, gitlab.GitlabObject): o.display(verbose) print("") else: print(o) elif isinstance(ret_val, dict): for k, v in six.iteritems(ret_val): print("{} = {}".format(k, v)) elif isinstance(ret_val, gitlab.base.GitlabObject): ret_val.display(verbose) elif isinstance(ret_val, six.string_types): print(ret_val) python-gitlab-1.3.0/gitlab/v3/objects.py000066400000000000000000002416071324224150200200760ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Copyright (C) 2013-2017 Gauvain Pocentek # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . from __future__ import print_function from __future__ import absolute_import import base64 import json import six from six.moves import urllib import gitlab from gitlab.base import * # noqa from gitlab.exceptions import * # noqa from gitlab import utils class SidekiqManager(object): """Manager for the Sidekiq methods. This manager doesn't actually manage objects but provides helper fonction for the sidekiq metrics API. """ def __init__(self, gl): """Constructs a Sidekiq manager. Args: gl (gitlab.Gitlab): Gitlab object referencing the GitLab server. """ self.gitlab = gl def _simple_get(self, url, **kwargs): r = self.gitlab._raw_get(url, **kwargs) raise_error_from_response(r, GitlabGetError) return r.json() def queue_metrics(self, **kwargs): """Returns the registred queues information.""" return self._simple_get('/sidekiq/queue_metrics', **kwargs) def process_metrics(self, **kwargs): """Returns the registred sidekiq workers.""" return self._simple_get('/sidekiq/process_metrics', **kwargs) def job_stats(self, **kwargs): """Returns statistics about the jobs performed.""" return self._simple_get('/sidekiq/job_stats', **kwargs) def compound_metrics(self, **kwargs): """Returns all available metrics and statistics.""" return self._simple_get('/sidekiq/compound_metrics', **kwargs) class UserEmail(GitlabObject): _url = '/users/%(user_id)s/emails' canUpdate = False shortPrintAttr = 'email' requiredUrlAttrs = ['user_id'] requiredCreateAttrs = ['email'] class UserEmailManager(BaseManager): obj_cls = UserEmail class UserKey(GitlabObject): _url = '/users/%(user_id)s/keys' canGet = 'from_list' canUpdate = False requiredUrlAttrs = ['user_id'] requiredCreateAttrs = ['title', 'key'] class UserKeyManager(BaseManager): obj_cls = UserKey class UserProject(GitlabObject): _url = '/projects/user/%(user_id)s' _constructorTypes = {'owner': 'User', 'namespace': 'Group'} canUpdate = False canDelete = False canList = False canGet = False requiredUrlAttrs = ['user_id'] requiredCreateAttrs = ['name'] optionalCreateAttrs = ['default_branch', 'issues_enabled', 'wall_enabled', 'merge_requests_enabled', 'wiki_enabled', 'snippets_enabled', 'public', 'visibility_level', 'description', 'builds_enabled', 'public_builds', 'import_url', 'only_allow_merge_if_build_succeeds'] class UserProjectManager(BaseManager): obj_cls = UserProject class User(GitlabObject): _url = '/users' shortPrintAttr = 'username' requiredCreateAttrs = ['email', 'username', 'name'] optionalCreateAttrs = ['password', 'reset_password', 'skype', 'linkedin', 'twitter', 'projects_limit', 'extern_uid', 'provider', 'bio', 'admin', 'can_create_group', 'website_url', 'confirm', 'external', 'organization', 'location'] requiredUpdateAttrs = ['email', 'username', 'name'] optionalUpdateAttrs = ['password', 'skype', 'linkedin', 'twitter', 'projects_limit', 'extern_uid', 'provider', 'bio', 'admin', 'can_create_group', 'website_url', 'confirm', 'external', 'organization', 'location'] managers = ( ('emails', 'UserEmailManager', [('user_id', 'id')]), ('keys', 'UserKeyManager', [('user_id', 'id')]), ('projects', 'UserProjectManager', [('user_id', 'id')]), ) def _data_for_gitlab(self, extra_parameters={}, update=False, as_json=True): if hasattr(self, 'confirm'): self.confirm = str(self.confirm).lower() return super(User, self)._data_for_gitlab(extra_parameters) def block(self, **kwargs): """Blocks the user.""" url = '/users/%s/block' % self.id r = self.gitlab._raw_put(url, **kwargs) raise_error_from_response(r, GitlabBlockError) self.state = 'blocked' def unblock(self, **kwargs): """Unblocks the user.""" url = '/users/%s/unblock' % self.id r = self.gitlab._raw_put(url, **kwargs) raise_error_from_response(r, GitlabUnblockError) self.state = 'active' def __eq__(self, other): if type(other) is type(self): selfdict = self.as_dict() otherdict = other.as_dict() selfdict.pop('password', None) otherdict.pop('password', None) return selfdict == otherdict return False class UserManager(BaseManager): obj_cls = User def search(self, query, **kwargs): """Search users. Args: query (str): The query string to send to GitLab for the search. all (bool): If True, return all the items, without pagination **kwargs: Additional arguments to send to GitLab. Returns: list(User): A list of matching users. Raises: GitlabConnectionError: If the server cannot be reached. GitlabListError: If the server fails to perform the request. """ url = self.obj_cls._url + '?search=' + query return self.gitlab._raw_list(url, self.obj_cls, **kwargs) def get_by_username(self, username, **kwargs): """Get a user by its username. Args: username (str): The name of the user. **kwargs: Additional arguments to send to GitLab. Returns: User: The matching user. Raises: GitlabConnectionError: If the server cannot be reached. GitlabGetError: If the server fails to perform the request. """ url = self.obj_cls._url + '?username=' + username results = self.gitlab._raw_list(url, self.obj_cls, **kwargs) assert len(results) in (0, 1) try: return results[0] except IndexError: raise GitlabGetError('no such user: ' + username) class CurrentUserEmail(GitlabObject): _url = '/user/emails' canUpdate = False shortPrintAttr = 'email' requiredCreateAttrs = ['email'] class CurrentUserEmailManager(BaseManager): obj_cls = CurrentUserEmail class CurrentUserKey(GitlabObject): _url = '/user/keys' canUpdate = False shortPrintAttr = 'title' requiredCreateAttrs = ['title', 'key'] class CurrentUserKeyManager(BaseManager): obj_cls = CurrentUserKey class CurrentUser(GitlabObject): _url = '/user' canList = False canCreate = False canUpdate = False canDelete = False shortPrintAttr = 'username' managers = ( ('emails', 'CurrentUserEmailManager', [('user_id', 'id')]), ('keys', 'CurrentUserKeyManager', [('user_id', 'id')]), ) class ApplicationSettings(GitlabObject): _url = '/application/settings' _id_in_update_url = False getRequiresId = False optionalUpdateAttrs = ['after_sign_out_path', 'container_registry_token_expire_delay', 'default_branch_protection', 'default_project_visibility', 'default_projects_limit', 'default_snippet_visibility', 'domain_blacklist', 'domain_blacklist_enabled', 'domain_whitelist', 'enabled_git_access_protocol', 'gravatar_enabled', 'home_page_url', 'max_attachment_size', 'repository_storage', 'restricted_signup_domains', 'restricted_visibility_levels', 'session_expire_delay', 'sign_in_text', 'signin_enabled', 'signup_enabled', 'twitter_sharing_enabled', 'user_oauth_applications'] canList = False canCreate = False canDelete = False 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 class BroadcastMessage(GitlabObject): _url = '/broadcast_messages' requiredCreateAttrs = ['message'] optionalCreateAttrs = ['starts_at', 'ends_at', 'color', 'font'] requiredUpdateAttrs = [] optionalUpdateAttrs = ['message', 'starts_at', 'ends_at', 'color', 'font'] class BroadcastMessageManager(BaseManager): obj_cls = BroadcastMessage class DeployKey(GitlabObject): _url = '/deploy_keys' canGet = 'from_list' canCreate = False canUpdate = False canDelete = False class DeployKeyManager(BaseManager): obj_cls = DeployKey class NotificationSettings(GitlabObject): _url = '/notification_settings' _id_in_update_url = False getRequiresId = False optionalUpdateAttrs = ['level', 'notification_email', 'new_note', 'new_issue', 'reopen_issue', 'close_issue', 'reassign_issue', 'new_merge_request', 'reopen_merge_request', 'close_merge_request', 'reassign_merge_request', 'merge_merge_request'] canList = False canCreate = False canDelete = False class NotificationSettingsManager(BaseManager): obj_cls = NotificationSettings class Gitignore(GitlabObject): _url = '/templates/gitignores' canDelete = False canUpdate = False canCreate = False idAttr = 'name' class GitignoreManager(BaseManager): obj_cls = Gitignore class Gitlabciyml(GitlabObject): _url = '/templates/gitlab_ci_ymls' canDelete = False canUpdate = False canCreate = False idAttr = 'name' class GitlabciymlManager(BaseManager): obj_cls = Gitlabciyml class GroupIssue(GitlabObject): _url = '/groups/%(group_id)s/issues' canGet = 'from_list' canCreate = False canUpdate = False canDelete = False requiredUrlAttrs = ['group_id'] optionalListAttrs = ['state', 'labels', 'milestone', 'order_by', 'sort'] class GroupIssueManager(BaseManager): obj_cls = GroupIssue class GroupMember(GitlabObject): _url = '/groups/%(group_id)s/members' canGet = 'from_list' requiredUrlAttrs = ['group_id'] requiredCreateAttrs = ['access_level', 'user_id'] optionalCreateAttrs = ['expires_at'] requiredUpdateAttrs = ['access_level'] optionalCreateAttrs = ['expires_at'] shortPrintAttr = 'username' def _update(self, **kwargs): self.user_id = self.id super(GroupMember, self)._update(**kwargs) class GroupMemberManager(BaseManager): obj_cls = GroupMember class GroupNotificationSettings(NotificationSettings): _url = '/groups/%(group_id)s/notification_settings' requiredUrlAttrs = ['group_id'] class GroupNotificationSettingsManager(BaseManager): obj_cls = GroupNotificationSettings class GroupAccessRequest(GitlabObject): _url = '/groups/%(group_id)s/access_requests' canGet = 'from_list' canUpdate = False def approve(self, access_level=gitlab.DEVELOPER_ACCESS, **kwargs): """Approve an access request. Args: access_level (int): The access level for the user. Raises: GitlabConnectionError: If the server cannot be reached. GitlabUpdateError: If the server fails to perform the request. """ url = ('/groups/%(group_id)s/access_requests/%(id)s/approve' % {'group_id': self.group_id, 'id': self.id}) data = {'access_level': access_level} r = self.gitlab._raw_put(url, data=data, **kwargs) raise_error_from_response(r, GitlabUpdateError, 201) self._set_from_dict(r.json()) class GroupAccessRequestManager(BaseManager): obj_cls = GroupAccessRequest class Hook(GitlabObject): _url = '/hooks' canUpdate = False requiredCreateAttrs = ['url'] shortPrintAttr = 'url' class HookManager(BaseManager): obj_cls = Hook class Issue(GitlabObject): _url = '/issues' _constructorTypes = {'author': 'User', 'assignee': 'User', 'milestone': 'ProjectMilestone'} canGet = 'from_list' canDelete = False canUpdate = False canCreate = False shortPrintAttr = 'title' optionalListAttrs = ['state', 'labels', 'order_by', 'sort'] class IssueManager(BaseManager): obj_cls = Issue class License(GitlabObject): _url = '/licenses' canDelete = False canUpdate = False canCreate = False idAttr = 'key' optionalListAttrs = ['popular'] optionalGetAttrs = ['project', 'fullname'] class LicenseManager(BaseManager): obj_cls = License class Snippet(GitlabObject): _url = '/snippets' _constructorTypes = {'author': 'User'} requiredCreateAttrs = ['title', 'file_name', 'content'] optionalCreateAttrs = ['lifetime', 'visibility_level'] optionalUpdateAttrs = ['title', 'file_name', 'content', 'visibility_level'] shortPrintAttr = 'title' def raw(self, streamed=False, action=None, chunk_size=1024, **kwargs): """Return the raw content of a snippet. Args: streamed (bool): If True the data will be processed by chunks of `chunk_size` and each chunk is passed to `action` for treatment. action (callable): Callable responsible of dealing with chunk of data. chunk_size (int): Size of each chunk. Returns: str: The snippet content. Raises: GitlabConnectionError: If the server cannot be reached. GitlabGetError: If the server fails to perform the request. """ url = ("/snippets/%(snippet_id)s/raw" % {'snippet_id': self.id}) r = self.gitlab._raw_get(url, **kwargs) raise_error_from_response(r, GitlabGetError) return utils.response_content(r, streamed, action, chunk_size) class SnippetManager(BaseManager): obj_cls = Snippet def public(self, **kwargs): """List all the public snippets. Args: all (bool): If True, return all the items, without pagination **kwargs: Additional arguments to send to GitLab. Returns: list(gitlab.Gitlab.Snippet): The list of snippets. """ return self.gitlab._raw_list("/snippets/public", Snippet, **kwargs) class Namespace(GitlabObject): _url = '/namespaces' canGet = 'from_list' canUpdate = False canDelete = False canCreate = False optionalListAttrs = ['search'] class NamespaceManager(BaseManager): obj_cls = Namespace class ProjectBoardList(GitlabObject): _url = '/projects/%(project_id)s/boards/%(board_id)s/lists' requiredUrlAttrs = ['project_id', 'board_id'] _constructorTypes = {'label': 'ProjectLabel'} requiredCreateAttrs = ['label_id'] requiredUpdateAttrs = ['position'] class ProjectBoardListManager(BaseManager): obj_cls = ProjectBoardList class ProjectBoard(GitlabObject): _url = '/projects/%(project_id)s/boards' requiredUrlAttrs = ['project_id'] _constructorTypes = {'labels': 'ProjectBoardList'} canGet = 'from_list' canUpdate = False canCreate = False canDelete = False managers = ( ('lists', 'ProjectBoardListManager', [('project_id', 'project_id'), ('board_id', 'id')]), ) class ProjectBoardManager(BaseManager): obj_cls = ProjectBoard class ProjectBranch(GitlabObject): _url = '/projects/%(project_id)s/repository/branches' _constructorTypes = {'author': 'User', "committer": "User"} idAttr = 'name' canUpdate = False requiredUrlAttrs = ['project_id'] requiredCreateAttrs = ['branch_name', 'ref'] def protect(self, protect=True, **kwargs): """Protects the branch.""" url = self._url % {'project_id': self.project_id} action = 'protect' if protect else 'unprotect' url = "%s/%s/%s" % (url, self.name, action) r = self.gitlab._raw_put(url, data=None, content_type=None, **kwargs) raise_error_from_response(r, GitlabProtectError) if protect: self.protected = protect else: del self.protected def unprotect(self, **kwargs): """Unprotects the branch.""" self.protect(False, **kwargs) class ProjectBranchManager(BaseManager): obj_cls = ProjectBranch class ProjectBuild(GitlabObject): _url = '/projects/%(project_id)s/builds' _constructorTypes = {'user': 'User', 'commit': 'ProjectCommit', 'runner': 'Runner'} requiredUrlAttrs = ['project_id'] canDelete = False canUpdate = False canCreate = False def cancel(self, **kwargs): """Cancel the build.""" url = '/projects/%s/builds/%s/cancel' % (self.project_id, self.id) r = self.gitlab._raw_post(url) raise_error_from_response(r, GitlabBuildCancelError, 201) def retry(self, **kwargs): """Retry the build.""" url = '/projects/%s/builds/%s/retry' % (self.project_id, self.id) r = self.gitlab._raw_post(url) raise_error_from_response(r, GitlabBuildRetryError, 201) def play(self, **kwargs): """Trigger a build explicitly.""" url = '/projects/%s/builds/%s/play' % (self.project_id, self.id) r = self.gitlab._raw_post(url) raise_error_from_response(r, GitlabBuildPlayError) def erase(self, **kwargs): """Erase the build (remove build artifacts and trace).""" url = '/projects/%s/builds/%s/erase' % (self.project_id, self.id) r = self.gitlab._raw_post(url) raise_error_from_response(r, GitlabBuildEraseError, 201) def keep_artifacts(self, **kwargs): """Prevent artifacts from being delete when expiration is set. Raises: GitlabConnectionError: If the server cannot be reached. GitlabCreateError: If the request failed. """ url = ('/projects/%s/builds/%s/artifacts/keep' % (self.project_id, self.id)) r = self.gitlab._raw_post(url) raise_error_from_response(r, GitlabGetError, 200) def artifacts(self, streamed=False, action=None, chunk_size=1024, **kwargs): """Get the build artifacts. Args: streamed (bool): If True the data will be processed by chunks of `chunk_size` and each chunk is passed to `action` for treatment. action (callable): Callable responsible of dealing with chunk of data. chunk_size (int): Size of each chunk. Returns: str: The artifacts if `streamed` is False, None otherwise. Raises: GitlabConnectionError: If the server cannot be reached. GitlabGetError: If the artifacts are not available. """ url = '/projects/%s/builds/%s/artifacts' % (self.project_id, self.id) r = self.gitlab._raw_get(url, streamed=streamed, **kwargs) raise_error_from_response(r, GitlabGetError, 200) return utils.response_content(r, streamed, action, chunk_size) def trace(self, streamed=False, action=None, chunk_size=1024, **kwargs): """Get the build trace. Args: streamed (bool): If True the data will be processed by chunks of `chunk_size` and each chunk is passed to `action` for treatment. action (callable): Callable responsible of dealing with chunk of data. chunk_size (int): Size of each chunk. Returns: str: The trace. Raises: GitlabConnectionError: If the server cannot be reached. GitlabGetError: If the trace is not available. """ url = '/projects/%s/builds/%s/trace' % (self.project_id, self.id) r = self.gitlab._raw_get(url, streamed=streamed, **kwargs) raise_error_from_response(r, GitlabGetError, 200) return utils.response_content(r, streamed, action, chunk_size) class ProjectBuildManager(BaseManager): obj_cls = ProjectBuild class ProjectCommitStatus(GitlabObject): _url = '/projects/%(project_id)s/repository/commits/%(commit_id)s/statuses' _create_url = '/projects/%(project_id)s/statuses/%(commit_id)s' canUpdate = False canDelete = False requiredUrlAttrs = ['project_id', 'commit_id'] optionalGetAttrs = ['ref_name', 'stage', 'name', 'all'] requiredCreateAttrs = ['state'] optionalCreateAttrs = ['description', 'name', 'context', 'ref', 'target_url'] class ProjectCommitStatusManager(BaseManager): obj_cls = ProjectCommitStatus class ProjectCommitComment(GitlabObject): _url = '/projects/%(project_id)s/repository/commits/%(commit_id)s/comments' canUpdate = False canGet = False canDelete = False requiredUrlAttrs = ['project_id', 'commit_id'] requiredCreateAttrs = ['note'] optionalCreateAttrs = ['path', 'line', 'line_type'] class ProjectCommitCommentManager(BaseManager): obj_cls = ProjectCommitComment class ProjectCommit(GitlabObject): _url = '/projects/%(project_id)s/repository/commits' canDelete = False canUpdate = False requiredUrlAttrs = ['project_id'] requiredCreateAttrs = ['branch_name', 'commit_message', 'actions'] optionalCreateAttrs = ['author_email', 'author_name'] shortPrintAttr = 'title' managers = ( ('comments', 'ProjectCommitCommentManager', [('project_id', 'project_id'), ('commit_id', 'id')]), ('statuses', 'ProjectCommitStatusManager', [('project_id', 'project_id'), ('commit_id', 'id')]), ) def diff(self, **kwargs): """Generate the commit diff.""" url = ('/projects/%(project_id)s/repository/commits/%(commit_id)s/diff' % {'project_id': self.project_id, 'commit_id': self.id}) r = self.gitlab._raw_get(url, **kwargs) raise_error_from_response(r, GitlabGetError) return r.json() def blob(self, filepath, streamed=False, action=None, chunk_size=1024, **kwargs): """Generate the content of a file for this commit. Args: filepath (str): Path of the file to request. streamed (bool): If True the data will be processed by chunks of `chunk_size` and each chunk is passed to `action` for treatment. action (callable): Callable responsible of dealing with chunk of data. chunk_size (int): Size of each chunk. Returns: str: The content of the file Raises: GitlabConnectionError: If the server cannot be reached. GitlabGetError: If the server fails to perform the request. """ url = ('/projects/%(project_id)s/repository/blobs/%(commit_id)s' % {'project_id': self.project_id, 'commit_id': self.id}) url += '?filepath=%s' % filepath r = self.gitlab._raw_get(url, streamed=streamed, **kwargs) raise_error_from_response(r, GitlabGetError) return utils.response_content(r, streamed, action, chunk_size) def builds(self, **kwargs): """List the build for this commit. Returns: list(ProjectBuild): A list of builds. Raises: GitlabConnectionError: If the server cannot be reached. GitlabListError: If the server fails to perform the request. """ url = '/projects/%s/repository/commits/%s/builds' % (self.project_id, self.id) return self.gitlab._raw_list(url, ProjectBuild, **kwargs) def cherry_pick(self, branch, **kwargs): """Cherry-pick a commit into a branch. Args: branch (str): Name of target branch. Raises: GitlabCherryPickError: If the cherry pick could not be applied. """ url = ('/projects/%s/repository/commits/%s/cherry_pick' % (self.project_id, self.id)) r = self.gitlab._raw_post(url, data={'project_id': self.project_id, 'branch': branch}, **kwargs) errors = {400: GitlabCherryPickError} raise_error_from_response(r, errors, expected_code=201) class ProjectCommitManager(BaseManager): obj_cls = ProjectCommit class ProjectEnvironment(GitlabObject): _url = '/projects/%(project_id)s/environments' canGet = 'from_list' requiredUrlAttrs = ['project_id'] requiredCreateAttrs = ['name'] optionalCreateAttrs = ['external_url'] optionalUpdateAttrs = ['name', 'external_url'] class ProjectEnvironmentManager(BaseManager): obj_cls = ProjectEnvironment class ProjectKey(GitlabObject): _url = '/projects/%(project_id)s/keys' canUpdate = False requiredUrlAttrs = ['project_id'] requiredCreateAttrs = ['title', 'key'] 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.parent.id, key_id) r = self.gitlab._raw_post(url) raise_error_from_response(r, GitlabProjectDeployKeyError, 201) def disable(self, key_id): """Disable a deploy key for a project.""" 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 ProjectEvent(GitlabObject): _url = '/projects/%(project_id)s/events' canGet = 'from_list' canDelete = False canUpdate = False canCreate = False requiredUrlAttrs = ['project_id'] shortPrintAttr = 'target_title' class ProjectEventManager(BaseManager): obj_cls = ProjectEvent class ProjectFork(GitlabObject): _url = '/projects/fork/%(project_id)s' canUpdate = False canDelete = False canList = False canGet = False requiredUrlAttrs = ['project_id'] optionalCreateAttrs = ['namespace'] class ProjectForkManager(BaseManager): obj_cls = ProjectFork class ProjectHook(GitlabObject): _url = '/projects/%(project_id)s/hooks' requiredUrlAttrs = ['project_id'] requiredCreateAttrs = ['url'] optionalCreateAttrs = ['push_events', 'issues_events', 'note_events', 'merge_requests_events', 'tag_push_events', 'build_events', 'enable_ssl_verification', 'token', 'pipeline_events'] shortPrintAttr = 'url' class ProjectHookManager(BaseManager): obj_cls = ProjectHook class ProjectIssueNote(GitlabObject): _url = '/projects/%(project_id)s/issues/%(issue_id)s/notes' _constructorTypes = {'author': 'User'} canDelete = False requiredUrlAttrs = ['project_id', 'issue_id'] requiredCreateAttrs = ['body'] optionalCreateAttrs = ['created_at'] # file attachment settings (see #56) description_attr = "body" project_id_attr = "project_id" class ProjectIssueNoteManager(BaseManager): obj_cls = ProjectIssueNote class ProjectIssue(GitlabObject): _url = '/projects/%(project_id)s/issues/' _constructorTypes = {'author': 'User', 'assignee': 'User', 'milestone': 'ProjectMilestone'} optionalListAttrs = ['state', 'labels', 'milestone', 'iid', 'order_by', 'sort'] requiredUrlAttrs = ['project_id'] requiredCreateAttrs = ['title'] optionalCreateAttrs = ['description', 'assignee_id', 'milestone_id', 'labels', 'created_at', 'due_date'] optionalUpdateAttrs = ['title', 'description', 'assignee_id', 'milestone_id', 'labels', 'created_at', 'updated_at', 'state_event', 'due_date'] shortPrintAttr = 'title' managers = ( ('notes', 'ProjectIssueNoteManager', [('project_id', 'project_id'), ('issue_id', 'id')]), ) # file attachment settings (see #56) description_attr = "description" project_id_attr = "project_id" def subscribe(self, **kwargs): """Subscribe to an issue. Raises: GitlabConnectionError: If the server cannot be reached. GitlabSubscribeError: If the subscription cannot be done """ url = ('/projects/%(project_id)s/issues/%(issue_id)s/subscription' % {'project_id': self.project_id, 'issue_id': self.id}) r = self.gitlab._raw_post(url, **kwargs) raise_error_from_response(r, GitlabSubscribeError, 201) self._set_from_dict(r.json()) def unsubscribe(self, **kwargs): """Unsubscribe an issue. Raises: GitlabConnectionError: If the server cannot be reached. GitlabUnsubscribeError: If the unsubscription cannot be done """ url = ('/projects/%(project_id)s/issues/%(issue_id)s/subscription' % {'project_id': self.project_id, 'issue_id': self.id}) r = self.gitlab._raw_delete(url, **kwargs) raise_error_from_response(r, GitlabUnsubscribeError) self._set_from_dict(r.json()) def move(self, to_project_id, **kwargs): """Move the issue to another project. Raises: GitlabConnectionError: If the server cannot be reached. """ url = ('/projects/%(project_id)s/issues/%(issue_id)s/move' % {'project_id': self.project_id, 'issue_id': self.id}) data = {'to_project_id': to_project_id} data.update(**kwargs) r = self.gitlab._raw_post(url, data=data) raise_error_from_response(r, GitlabUpdateError, 201) self._set_from_dict(r.json()) def todo(self, **kwargs): """Create a todo for the issue. Raises: GitlabConnectionError: If the server cannot be reached. """ url = ('/projects/%(project_id)s/issues/%(issue_id)s/todo' % {'project_id': self.project_id, 'issue_id': self.id}) r = self.gitlab._raw_post(url, **kwargs) raise_error_from_response(r, GitlabTodoError, [201, 304]) def time_stats(self, **kwargs): """Get time stats for the issue. Raises: GitlabConnectionError: If the server cannot be reached. """ url = ('/projects/%(project_id)s/issues/%(issue_id)s/time_stats' % {'project_id': self.project_id, 'issue_id': self.id}) r = self.gitlab._raw_get(url, **kwargs) raise_error_from_response(r, GitlabGetError) return r.json() def time_estimate(self, **kwargs): """Set an estimated time of work for the issue. Raises: GitlabConnectionError: If the server cannot be reached. """ url = ('/projects/%(project_id)s/issues/%(issue_id)s/time_estimate' % {'project_id': self.project_id, 'issue_id': self.id}) r = self.gitlab._raw_post(url, **kwargs) raise_error_from_response(r, GitlabTimeTrackingError, 201) return r.json() def reset_time_estimate(self, **kwargs): """Resets estimated time for the issue to 0 seconds. Raises: GitlabConnectionError: If the server cannot be reached. """ url = ('/projects/%(project_id)s/issues/%(issue_id)s/' 'reset_time_estimate' % {'project_id': self.project_id, 'issue_id': self.id}) r = self.gitlab._raw_post(url, **kwargs) raise_error_from_response(r, GitlabTimeTrackingError, 200) return r.json() def add_spent_time(self, **kwargs): """Set an estimated time of work for the issue. Raises: GitlabConnectionError: If the server cannot be reached. """ url = ('/projects/%(project_id)s/issues/%(issue_id)s/' 'add_spent_time' % {'project_id': self.project_id, 'issue_id': self.id}) r = self.gitlab._raw_post(url, **kwargs) raise_error_from_response(r, GitlabTimeTrackingError, 200) return r.json() def reset_spent_time(self, **kwargs): """Set an estimated time of work for the issue. Raises: GitlabConnectionError: If the server cannot be reached. """ url = ('/projects/%(project_id)s/issues/%(issue_id)s/' 'reset_spent_time' % {'project_id': self.project_id, 'issue_id': self.id}) r = self.gitlab._raw_post(url, **kwargs) raise_error_from_response(r, GitlabTimeTrackingError, 200) return r.json() class ProjectIssueManager(BaseManager): obj_cls = ProjectIssue class ProjectMember(GitlabObject): _url = '/projects/%(project_id)s/members' requiredUrlAttrs = ['project_id'] requiredCreateAttrs = ['access_level', 'user_id'] optionalCreateAttrs = ['expires_at'] requiredUpdateAttrs = ['access_level'] optionalCreateAttrs = ['expires_at'] shortPrintAttr = 'username' class ProjectMemberManager(BaseManager): obj_cls = ProjectMember class ProjectNote(GitlabObject): _url = '/projects/%(project_id)s/notes' _constructorTypes = {'author': 'User'} canUpdate = False canDelete = False requiredUrlAttrs = ['project_id'] requiredCreateAttrs = ['body'] class ProjectNoteManager(BaseManager): obj_cls = ProjectNote class ProjectNotificationSettings(NotificationSettings): _url = '/projects/%(project_id)s/notification_settings' requiredUrlAttrs = ['project_id'] class ProjectNotificationSettingsManager(BaseManager): obj_cls = ProjectNotificationSettings class ProjectTagRelease(GitlabObject): _url = '/projects/%(project_id)s/repository/tags/%(tag_name)/release' canDelete = False canList = False requiredUrlAttrs = ['project_id', 'tag_name'] requiredCreateAttrs = ['description'] shortPrintAttr = 'description' class ProjectTag(GitlabObject): _url = '/projects/%(project_id)s/repository/tags' _constructorTypes = {'release': 'ProjectTagRelease', 'commit': 'ProjectCommit'} idAttr = 'name' canGet = 'from_list' canUpdate = False requiredUrlAttrs = ['project_id'] requiredCreateAttrs = ['tag_name', 'ref'] optionalCreateAttrs = ['message'] shortPrintAttr = 'name' def set_release_description(self, description): """Set the release notes on the tag. If the release doesn't exist yet, it will be created. If it already exists, its description will be updated. Args: description (str): Description of the release. Raises: GitlabConnectionError: If the server cannot be reached. GitlabCreateError: If the server fails to create the release. GitlabUpdateError: If the server fails to update the release. """ url = '/projects/%s/repository/tags/%s/release' % (self.project_id, self.name) if self.release is None: r = self.gitlab._raw_post(url, data={'description': description}) raise_error_from_response(r, GitlabCreateError, 201) else: r = self.gitlab._raw_put(url, data={'description': description}) raise_error_from_response(r, GitlabUpdateError, 200) self.release = ProjectTagRelease(self, r.json()) class ProjectTagManager(BaseManager): obj_cls = ProjectTag class ProjectMergeRequestDiff(GitlabObject): _url = ('/projects/%(project_id)s/merge_requests/' '%(merge_request_id)s/versions') canCreate = False canUpdate = False canDelete = False requiredUrlAttrs = ['project_id', 'merge_request_id'] class ProjectMergeRequestDiffManager(BaseManager): obj_cls = ProjectMergeRequestDiff class ProjectMergeRequestNote(GitlabObject): _url = '/projects/%(project_id)s/merge_requests/%(merge_request_id)s/notes' _constructorTypes = {'author': 'User'} requiredUrlAttrs = ['project_id', 'merge_request_id'] requiredCreateAttrs = ['body'] class ProjectMergeRequestNoteManager(BaseManager): obj_cls = ProjectMergeRequestNote class ProjectMergeRequest(GitlabObject): _url = '/projects/%(project_id)s/merge_requests' _constructorTypes = {'author': 'User', 'assignee': 'User'} requiredUrlAttrs = ['project_id'] requiredCreateAttrs = ['source_branch', 'target_branch', 'title'] optionalCreateAttrs = ['assignee_id', 'description', 'target_project_id', 'labels', 'milestone_id', 'remove_source_branch'] optionalUpdateAttrs = ['target_branch', 'assignee_id', 'title', 'description', 'state_event', 'labels', 'milestone_id'] optionalListAttrs = ['iid', 'state', 'order_by', 'sort'] managers = ( ('notes', 'ProjectMergeRequestNoteManager', [('project_id', 'project_id'), ('merge_request_id', 'id')]), ('diffs', 'ProjectMergeRequestDiffManager', [('project_id', 'project_id'), ('merge_request_id', 'id')]), ) def _data_for_gitlab(self, extra_parameters={}, update=False, as_json=True): data = (super(ProjectMergeRequest, self) ._data_for_gitlab(extra_parameters, update=update, as_json=False)) if update: # Drop source_branch attribute as it is not accepted by the gitlab # server (Issue #76) data.pop('source_branch', None) return json.dumps(data) def subscribe(self, **kwargs): """Subscribe to a MR. Raises: GitlabConnectionError: If the server cannot be reached. GitlabSubscribeError: If the subscription cannot be done """ url = ('/projects/%(project_id)s/merge_requests/%(mr_id)s/' 'subscription' % {'project_id': self.project_id, 'mr_id': self.id}) r = self.gitlab._raw_post(url, **kwargs) raise_error_from_response(r, GitlabSubscribeError, [201, 304]) if r.status_code == 201: self._set_from_dict(r.json()) def unsubscribe(self, **kwargs): """Unsubscribe a MR. Raises: GitlabConnectionError: If the server cannot be reached. GitlabUnsubscribeError: If the unsubscription cannot be done """ url = ('/projects/%(project_id)s/merge_requests/%(mr_id)s/' 'subscription' % {'project_id': self.project_id, 'mr_id': self.id}) r = self.gitlab._raw_delete(url, **kwargs) raise_error_from_response(r, GitlabUnsubscribeError, [200, 304]) if r.status_code == 200: self._set_from_dict(r.json()) def cancel_merge_when_build_succeeds(self, **kwargs): """Cancel merge when build succeeds.""" u = ('/projects/%s/merge_requests/%s/cancel_merge_when_build_succeeds' % (self.project_id, self.id)) r = self.gitlab._raw_put(u, **kwargs) errors = {401: GitlabMRForbiddenError, 405: GitlabMRClosedError, 406: GitlabMROnBuildSuccessError} raise_error_from_response(r, errors) return ProjectMergeRequest(self, r.json()) def closes_issues(self, **kwargs): """List issues closed by the MR. Returns: list (ProjectIssue): List of closed issues Raises: GitlabConnectionError: If the server cannot be reached. GitlabGetError: If the server fails to perform the request. """ url = ('/projects/%s/merge_requests/%s/closes_issues' % (self.project_id, self.id)) return self.gitlab._raw_list(url, ProjectIssue, **kwargs) def commits(self, **kwargs): """List the merge request commits. Returns: list (ProjectCommit): List of commits Raises: GitlabConnectionError: If the server cannot be reached. GitlabListError: If the server fails to perform the request. """ url = ('/projects/%s/merge_requests/%s/commits' % (self.project_id, self.id)) return self.gitlab._raw_list(url, ProjectCommit, **kwargs) def changes(self, **kwargs): """List the merge request changes. Returns: list (dict): List of changes Raises: GitlabConnectionError: If the server cannot be reached. GitlabListError: If the server fails to perform the request. """ url = ('/projects/%s/merge_requests/%s/changes' % (self.project_id, self.id)) r = self.gitlab._raw_get(url, **kwargs) raise_error_from_response(r, GitlabListError) return r.json() def merge(self, merge_commit_message=None, should_remove_source_branch=False, merge_when_build_succeeds=False, **kwargs): """Accept the merge request. Args: merge_commit_message (bool): Commit message should_remove_source_branch (bool): If True, removes the source branch merge_when_build_succeeds (bool): Wait for the build to succeed, then merge Returns: ProjectMergeRequest: The updated MR Raises: GitlabConnectionError: If the server cannot be reached. GitlabMRForbiddenError: If the user doesn't have permission to close thr MR GitlabMRClosedError: If the MR is already closed """ url = '/projects/%s/merge_requests/%s/merge' % (self.project_id, self.id) data = {} if merge_commit_message: data['merge_commit_message'] = merge_commit_message if should_remove_source_branch: data['should_remove_source_branch'] = True if merge_when_build_succeeds: data['merge_when_build_succeeds'] = True r = self.gitlab._raw_put(url, data=data, **kwargs) errors = {401: GitlabMRForbiddenError, 405: GitlabMRClosedError} raise_error_from_response(r, errors) self._set_from_dict(r.json()) def todo(self, **kwargs): """Create a todo for the merge request. Raises: GitlabConnectionError: If the server cannot be reached. """ url = ('/projects/%(project_id)s/merge_requests/%(mr_id)s/todo' % {'project_id': self.project_id, 'mr_id': self.id}) r = self.gitlab._raw_post(url, **kwargs) raise_error_from_response(r, GitlabTodoError, [201, 304]) def time_stats(self, **kwargs): """Get time stats for the merge request. Raises: GitlabConnectionError: If the server cannot be reached. """ url = ('/projects/%(project_id)s/merge_requests/%(mr_id)s/time_stats' % {'project_id': self.project_id, 'mr_id': self.id}) r = self.gitlab._raw_get(url, **kwargs) raise_error_from_response(r, GitlabGetError) return r.json() def time_estimate(self, **kwargs): """Set an estimated time of work for the merge request. Raises: GitlabConnectionError: If the server cannot be reached. """ url = ('/projects/%(project_id)s/merge_requests/%(mr_id)s/' 'time_estimate' % {'project_id': self.project_id, 'mr_id': self.id}) r = self.gitlab._raw_post(url, **kwargs) raise_error_from_response(r, GitlabTimeTrackingError, 201) return r.json() def reset_time_estimate(self, **kwargs): """Resets estimated time for the merge request to 0 seconds. Raises: GitlabConnectionError: If the server cannot be reached. """ url = ('/projects/%(project_id)s/merge_requests/%(mr_id)s/' 'reset_time_estimate' % {'project_id': self.project_id, 'mr_id': self.id}) r = self.gitlab._raw_post(url, **kwargs) raise_error_from_response(r, GitlabTimeTrackingError, 200) return r.json() def add_spent_time(self, **kwargs): """Set an estimated time of work for the merge request. Raises: GitlabConnectionError: If the server cannot be reached. """ url = ('/projects/%(project_id)s/merge_requests/%(mr_id)s/' 'add_spent_time' % {'project_id': self.project_id, 'mr_id': self.id}) r = self.gitlab._raw_post(url, **kwargs) raise_error_from_response(r, GitlabTimeTrackingError, 200) return r.json() def reset_spent_time(self, **kwargs): """Set an estimated time of work for the merge request. Raises: GitlabConnectionError: If the server cannot be reached. """ url = ('/projects/%(project_id)s/merge_requests/%(mr_id)s/' 'reset_spent_time' % {'project_id': self.project_id, 'mr_id': self.id}) r = self.gitlab._raw_post(url, **kwargs) raise_error_from_response(r, GitlabTimeTrackingError, 200) return r.json() class ProjectMergeRequestManager(BaseManager): obj_cls = ProjectMergeRequest class ProjectMilestone(GitlabObject): _url = '/projects/%(project_id)s/milestones' canDelete = False requiredUrlAttrs = ['project_id'] optionalListAttrs = ['iid', 'state'] requiredCreateAttrs = ['title'] optionalCreateAttrs = ['description', 'due_date', 'start_date', 'state_event'] optionalUpdateAttrs = requiredCreateAttrs + optionalCreateAttrs shortPrintAttr = 'title' def issues(self, **kwargs): url = "/projects/%s/milestones/%s/issues" % (self.project_id, self.id) return self.gitlab._raw_list(url, ProjectIssue, **kwargs) def merge_requests(self, **kwargs): """List the merge requests related to this milestone Returns: list (ProjectMergeRequest): List of merge requests Raises: GitlabConnectionError: If the server cannot be reached. GitlabListError: If the server fails to perform the request. """ url = ('/projects/%s/milestones/%s/merge_requests' % (self.project_id, self.id)) return self.gitlab._raw_list(url, ProjectMergeRequest, **kwargs) class ProjectMilestoneManager(BaseManager): obj_cls = ProjectMilestone class ProjectLabel(GitlabObject): _url = '/projects/%(project_id)s/labels' _id_in_delete_url = False _id_in_update_url = False canGet = 'from_list' requiredUrlAttrs = ['project_id'] idAttr = 'name' requiredDeleteAttrs = ['name'] requiredCreateAttrs = ['name', 'color'] optionalCreateAttrs = ['description', 'priority'] requiredUpdateAttrs = ['name'] optionalUpdateAttrs = ['new_name', 'color', 'description', 'priority'] def subscribe(self, **kwargs): """Subscribe to a label. Raises: GitlabConnectionError: If the server cannot be reached. GitlabSubscribeError: If the subscription cannot be done """ url = ('/projects/%(project_id)s/labels/%(label_id)s/subscription' % {'project_id': self.project_id, 'label_id': self.name}) r = self.gitlab._raw_post(url, **kwargs) raise_error_from_response(r, GitlabSubscribeError, [201, 304]) self._set_from_dict(r.json()) def unsubscribe(self, **kwargs): """Unsubscribe a label. Raises: GitlabConnectionError: If the server cannot be reached. GitlabUnsubscribeError: If the unsubscription cannot be done """ url = ('/projects/%(project_id)s/labels/%(label_id)s/subscription' % {'project_id': self.project_id, 'label_id': self.name}) r = self.gitlab._raw_delete(url, **kwargs) raise_error_from_response(r, GitlabUnsubscribeError, [200, 304]) self._set_from_dict(r.json()) class ProjectLabelManager(BaseManager): obj_cls = ProjectLabel class ProjectFile(GitlabObject): _url = '/projects/%(project_id)s/repository/files' canList = False requiredUrlAttrs = ['project_id'] requiredGetAttrs = ['file_path', 'ref'] requiredCreateAttrs = ['file_path', 'branch_name', 'content', 'commit_message'] optionalCreateAttrs = ['encoding'] requiredDeleteAttrs = ['branch_name', 'commit_message', 'file_path'] shortPrintAttr = 'file_path' getRequiresId = False def decode(self): """Returns the decoded content of the file. Returns: (str): the decoded content. """ return base64.b64decode(self.content) class ProjectFileManager(BaseManager): obj_cls = ProjectFile class ProjectPipeline(GitlabObject): _url = '/projects/%(project_id)s/pipelines' _create_url = '/projects/%(project_id)s/pipeline' canUpdate = False canDelete = False requiredUrlAttrs = ['project_id'] requiredCreateAttrs = ['ref'] def retry(self, **kwargs): """Retries failed builds in a pipeline. Raises: GitlabConnectionError: If the server cannot be reached. GitlabPipelineRetryError: If the retry cannot be done. """ url = ('/projects/%(project_id)s/pipelines/%(id)s/retry' % {'project_id': self.project_id, 'id': self.id}) r = self.gitlab._raw_post(url, data=None, content_type=None, **kwargs) raise_error_from_response(r, GitlabPipelineRetryError, 201) self._set_from_dict(r.json()) def cancel(self, **kwargs): """Cancel builds in a pipeline. Raises: GitlabConnectionError: If the server cannot be reached. GitlabPipelineCancelError: If the retry cannot be done. """ url = ('/projects/%(project_id)s/pipelines/%(id)s/cancel' % {'project_id': self.project_id, 'id': self.id}) r = self.gitlab._raw_post(url, data=None, content_type=None, **kwargs) raise_error_from_response(r, GitlabPipelineRetryError, 200) self._set_from_dict(r.json()) class ProjectPipelineManager(BaseManager): obj_cls = ProjectPipeline class ProjectSnippetNote(GitlabObject): _url = '/projects/%(project_id)s/snippets/%(snippet_id)s/notes' _constructorTypes = {'author': 'User'} canUpdate = False canDelete = False requiredUrlAttrs = ['project_id', 'snippet_id'] requiredCreateAttrs = ['body'] class ProjectSnippetNoteManager(BaseManager): obj_cls = ProjectSnippetNote class ProjectSnippet(GitlabObject): _url = '/projects/%(project_id)s/snippets' _constructorTypes = {'author': 'User'} requiredUrlAttrs = ['project_id'] requiredCreateAttrs = ['title', 'file_name', 'code'] optionalCreateAttrs = ['lifetime', 'visibility_level'] optionalUpdateAttrs = ['title', 'file_name', 'code', 'visibility_level'] shortPrintAttr = 'title' managers = ( ('notes', 'ProjectSnippetNoteManager', [('project_id', 'project_id'), ('snippet_id', 'id')]), ) def content(self, streamed=False, action=None, chunk_size=1024, **kwargs): """Return the raw content of a snippet. Args: streamed (bool): If True the data will be processed by chunks of `chunk_size` and each chunk is passed to `action` for treatment. action (callable): Callable responsible of dealing with chunk of data. chunk_size (int): Size of each chunk. Returns: str: The snippet content Raises: GitlabConnectionError: If the server cannot be reached. GitlabGetError: If the server fails to perform the request. """ url = ("/projects/%(project_id)s/snippets/%(snippet_id)s/raw" % {'project_id': self.project_id, 'snippet_id': self.id}) r = self.gitlab._raw_get(url, **kwargs) raise_error_from_response(r, GitlabGetError) return utils.response_content(r, streamed, action, chunk_size) class ProjectSnippetManager(BaseManager): obj_cls = ProjectSnippet class ProjectTrigger(GitlabObject): _url = '/projects/%(project_id)s/triggers' canUpdate = False idAttr = 'token' requiredUrlAttrs = ['project_id'] class ProjectTriggerManager(BaseManager): obj_cls = ProjectTrigger class ProjectVariable(GitlabObject): _url = '/projects/%(project_id)s/variables' idAttr = 'key' requiredUrlAttrs = ['project_id'] requiredCreateAttrs = ['key', 'value'] class ProjectVariableManager(BaseManager): obj_cls = ProjectVariable class ProjectService(GitlabObject): _url = '/projects/%(project_id)s/services/%(service_name)s' canList = False canCreate = False _id_in_update_url = False _id_in_delete_url = False getRequiresId = False requiredUrlAttrs = ['project_id', 'service_name'] _service_attrs = { 'asana': (('api_key', ), ('restrict_to_branch', )), 'assembla': (('token', ), ('subdomain', )), 'bamboo': (('bamboo_url', 'build_key', 'username', 'password'), tuple()), 'buildkite': (('token', 'project_url'), ('enable_ssl_verification', )), 'campfire': (('token', ), ('subdomain', 'room')), 'custom-issue-tracker': (('new_issue_url', 'issues_url', 'project_url'), ('description', 'title')), '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()), 'hipchat': (('token', ), ('color', 'notify', 'room', 'api_version', 'server')), 'irker': (('recipients', ), ('default_irc_uri', 'server_port', 'server_host', 'colorize_messages')), 'jira': (tuple(), ( # Required fields in GitLab >= 8.14 'url', 'project_key', # Required fields in GitLab < 8.14 'new_issue_url', 'project_url', 'issues_url', 'api_url', 'description', # Optional fields 'username', 'password', 'jira_issue_transition_id')), 'mattermost': (('webhook',), ('username', 'channel')), 'pivotaltracker': (('token', ), tuple()), 'pushover': (('api_key', 'user_key', 'priority'), ('device', 'sound')), 'redmine': (('new_issue_url', 'project_url', 'issues_url'), ('description', )), 'slack': (('webhook', ), ('username', 'channel')), 'teamcity': (('teamcity_url', 'build_type', 'username', 'password'), tuple()) } def _data_for_gitlab(self, extra_parameters={}, update=False, as_json=True): data = (super(ProjectService, self) ._data_for_gitlab(extra_parameters, update=update, as_json=False)) missing = [] # Mandatory args for attr in self._service_attrs[self.service_name][0]: if not hasattr(self, attr): missing.append(attr) else: data[attr] = getattr(self, attr) if missing: raise GitlabUpdateError('Missing attribute(s): %s' % ", ".join(missing)) # Optional args for attr in self._service_attrs[self.service_name][1]: if hasattr(self, attr): data[attr] = getattr(self, attr) return json.dumps(data) class ProjectServiceManager(BaseManager): obj_cls = ProjectService def available(self, **kwargs): """List the services known by python-gitlab. Returns: list (str): The list of service code names. """ return list(ProjectService._service_attrs.keys()) class ProjectAccessRequest(GitlabObject): _url = '/projects/%(project_id)s/access_requests' canGet = 'from_list' canUpdate = False def approve(self, access_level=gitlab.DEVELOPER_ACCESS, **kwargs): """Approve an access request. Args: access_level (int): The access level for the user. Raises: GitlabConnectionError: If the server cannot be reached. GitlabUpdateError: If the server fails to perform the request. """ url = ('/projects/%(project_id)s/access_requests/%(id)s/approve' % {'project_id': self.project_id, 'id': self.id}) data = {'access_level': access_level} r = self.gitlab._raw_put(url, data=data, **kwargs) raise_error_from_response(r, GitlabUpdateError, 201) self._set_from_dict(r.json()) class ProjectAccessRequestManager(BaseManager): obj_cls = ProjectAccessRequest class ProjectDeployment(GitlabObject): _url = '/projects/%(project_id)s/deployments' canCreate = False canUpdate = False canDelete = False 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'} optionalListAttrs = ['search'] requiredCreateAttrs = ['name'] optionalListAttrs = ['search'] optionalCreateAttrs = ['path', 'namespace_id', 'description', 'issues_enabled', 'merge_requests_enabled', 'builds_enabled', 'wiki_enabled', 'snippets_enabled', 'container_registry_enabled', 'shared_runners_enabled', 'public', 'visibility_level', 'import_url', 'public_builds', 'only_allow_merge_if_build_succeeds', 'only_allow_merge_if_all_discussions_are_resolved', 'lfs_enabled', 'request_access_enabled'] optionalUpdateAttrs = ['name', 'path', 'default_branch', 'description', 'issues_enabled', 'merge_requests_enabled', 'builds_enabled', 'wiki_enabled', 'snippets_enabled', 'container_registry_enabled', 'shared_runners_enabled', 'public', 'visibility_level', 'import_url', 'public_builds', 'only_allow_merge_if_build_succeeds', 'only_allow_merge_if_all_discussions_are_resolved', 'lfs_enabled', 'request_access_enabled'] shortPrintAttr = 'path' managers = ( ('accessrequests', 'ProjectAccessRequestManager', [('project_id', 'id')]), ('boards', 'ProjectBoardManager', [('project_id', 'id')]), ('board_lists', 'ProjectBoardListManager', [('project_id', 'id')]), ('branches', 'ProjectBranchManager', [('project_id', 'id')]), ('builds', 'ProjectBuildManager', [('project_id', 'id')]), ('commits', 'ProjectCommitManager', [('project_id', 'id')]), ('deployments', 'ProjectDeploymentManager', [('project_id', 'id')]), ('environments', 'ProjectEnvironmentManager', [('project_id', 'id')]), ('events', 'ProjectEventManager', [('project_id', 'id')]), ('files', 'ProjectFileManager', [('project_id', 'id')]), ('forks', 'ProjectForkManager', [('project_id', 'id')]), ('hooks', 'ProjectHookManager', [('project_id', 'id')]), ('keys', 'ProjectKeyManager', [('project_id', 'id')]), ('issues', 'ProjectIssueManager', [('project_id', 'id')]), ('labels', 'ProjectLabelManager', [('project_id', 'id')]), ('members', 'ProjectMemberManager', [('project_id', 'id')]), ('mergerequests', 'ProjectMergeRequestManager', [('project_id', 'id')]), ('milestones', 'ProjectMilestoneManager', [('project_id', 'id')]), ('notes', 'ProjectNoteManager', [('project_id', 'id')]), ('notificationsettings', 'ProjectNotificationSettingsManager', [('project_id', 'id')]), ('pipelines', 'ProjectPipelineManager', [('project_id', 'id')]), ('runners', 'ProjectRunnerManager', [('project_id', 'id')]), ('services', 'ProjectServiceManager', [('project_id', 'id')]), ('snippets', 'ProjectSnippetManager', [('project_id', 'id')]), ('tags', 'ProjectTagManager', [('project_id', 'id')]), ('triggers', 'ProjectTriggerManager', [('project_id', 'id')]), ('variables', 'ProjectVariableManager', [('project_id', 'id')]), ) VISIBILITY_PRIVATE = gitlab.VISIBILITY_PRIVATE VISIBILITY_INTERNAL = gitlab.VISIBILITY_INTERNAL VISIBILITY_PUBLIC = gitlab.VISIBILITY_PUBLIC def repository_tree(self, path='', ref_name='', **kwargs): """Return a list of files in the repository. Args: path (str): Path of the top folder (/ by default) ref_name (str): Reference to a commit or branch Returns: str: The json representation of the tree. Raises: GitlabConnectionError: If the server cannot be reached. GitlabGetError: If the server fails to perform the request. """ url = "/projects/%s/repository/tree" % (self.id) params = [] if path: params.append(urllib.parse.urlencode({'path': path})) if ref_name: params.append("ref_name=%s" % ref_name) if params: url += '?' + "&".join(params) r = self.gitlab._raw_get(url, **kwargs) raise_error_from_response(r, GitlabGetError) return r.json() def repository_blob(self, sha, filepath, streamed=False, action=None, chunk_size=1024, **kwargs): """Return the content of a file for a commit. Args: sha (str): ID of the commit filepath (str): Path of the file to return streamed (bool): If True the data will be processed by chunks of `chunk_size` and each chunk is passed to `action` for treatment. action (callable): Callable responsible of dealing with chunk of data. chunk_size (int): Size of each chunk. Returns: str: The file content Raises: GitlabConnectionError: If the server cannot be reached. GitlabGetError: If the server fails to perform the request. """ url = "/projects/%s/repository/blobs/%s" % (self.id, sha) url += '?%s' % (urllib.parse.urlencode({'filepath': filepath})) r = self.gitlab._raw_get(url, streamed=streamed, **kwargs) raise_error_from_response(r, GitlabGetError) return utils.response_content(r, streamed, action, chunk_size) def repository_raw_blob(self, sha, streamed=False, action=None, chunk_size=1024, **kwargs): """Returns the raw file contents for a blob by blob SHA. Args: sha(str): ID of the blob streamed (bool): If True the data will be processed by chunks of `chunk_size` and each chunk is passed to `action` for treatment. action (callable): Callable responsible of dealing with chunk of data. chunk_size (int): Size of each chunk. Returns: str: The blob content Raises: GitlabConnectionError: If the server cannot be reached. GitlabGetError: If the server fails to perform the request. """ url = "/projects/%s/repository/raw_blobs/%s" % (self.id, sha) r = self.gitlab._raw_get(url, streamed=streamed, **kwargs) raise_error_from_response(r, GitlabGetError) return utils.response_content(r, streamed, action, chunk_size) def repository_compare(self, from_, to, **kwargs): """Returns a diff between two branches/commits. Args: from_(str): orig branch/SHA to(str): dest branch/SHA Returns: str: The diff Raises: GitlabConnectionError: If the server cannot be reached. GitlabGetError: If the server fails to perform the request. """ url = "/projects/%s/repository/compare" % self.id url = "%s?from=%s&to=%s" % (url, from_, to) r = self.gitlab._raw_get(url, **kwargs) raise_error_from_response(r, GitlabGetError) return r.json() def repository_contributors(self): """Returns a list of contributors for the project. Returns: list: The contibutors Raises: GitlabConnectionError: If the server cannot be reached. GitlabGetError: If the server fails to perform the request. """ url = "/projects/%s/repository/contributors" % self.id r = self.gitlab._raw_get(url) raise_error_from_response(r, GitlabListError) return r.json() def repository_archive(self, sha=None, streamed=False, action=None, chunk_size=1024, **kwargs): """Return a tarball of the repository. Args: sha (str): ID of the commit (default branch by default). streamed (bool): If True the data will be processed by chunks of `chunk_size` and each chunk is passed to `action` for treatment. action (callable): Callable responsible of dealing with chunk of data. chunk_size (int): Size of each chunk. Returns: str: The binary data of the archive. Raises: GitlabConnectionError: If the server cannot be reached. GitlabGetError: If the server fails to perform the request. """ url = '/projects/%s/repository/archive' % self.id if sha: url += '?sha=%s' % sha r = self.gitlab._raw_get(url, streamed=streamed, **kwargs) raise_error_from_response(r, GitlabGetError) return utils.response_content(r, streamed, action, chunk_size) def create_fork_relation(self, forked_from_id): """Create a forked from/to relation between existing projects. Args: forked_from_id (int): The ID of the project that was forked from Raises: GitlabConnectionError: If the server cannot be reached. GitlabCreateError: If the server fails to perform the request. """ url = "/projects/%s/fork/%s" % (self.id, forked_from_id) r = self.gitlab._raw_post(url) raise_error_from_response(r, GitlabCreateError, 201) def delete_fork_relation(self): """Delete a forked relation between existing projects. Raises: GitlabConnectionError: If the server cannot be reached. GitlabDeleteError: If the server fails to perform the request. """ url = "/projects/%s/fork" % self.id r = self.gitlab._raw_delete(url) raise_error_from_response(r, GitlabDeleteError) def star(self, **kwargs): """Star a project. Returns: Project: the updated Project Raises: GitlabCreateError: If the action cannot be done GitlabConnectionError: If the server cannot be reached. """ url = "/projects/%s/star" % self.id r = self.gitlab._raw_post(url, **kwargs) raise_error_from_response(r, GitlabCreateError, [201, 304]) return Project(self.gitlab, r.json()) if r.status_code == 201 else self def unstar(self, **kwargs): """Unstar a project. Returns: Project: the updated Project Raises: GitlabDeleteError: If the action cannot be done GitlabConnectionError: If the server cannot be reached. """ url = "/projects/%s/star" % self.id r = self.gitlab._raw_delete(url, **kwargs) raise_error_from_response(r, GitlabDeleteError, [200, 304]) return Project(self.gitlab, r.json()) if r.status_code == 200 else self def archive(self, **kwargs): """Archive a project. Returns: Project: the updated Project Raises: GitlabCreateError: If the action cannot be done GitlabConnectionError: If the server cannot be reached. """ url = "/projects/%s/archive" % self.id r = self.gitlab._raw_post(url, **kwargs) raise_error_from_response(r, GitlabCreateError, 201) return Project(self.gitlab, r.json()) if r.status_code == 201 else self def unarchive(self, **kwargs): """Unarchive a project. Returns: Project: the updated Project Raises: GitlabDeleteError: If the action cannot be done GitlabConnectionError: If the server cannot be reached. """ url = "/projects/%s/unarchive" % self.id r = self.gitlab._raw_delete(url, **kwargs) raise_error_from_response(r, GitlabCreateError, 201) return Project(self.gitlab, r.json()) if r.status_code == 201 else self def share(self, group_id, group_access, **kwargs): """Share the project with a group. Args: group_id (int): ID of the group. group_access (int): Access level for the group. Raises: GitlabConnectionError: If the server cannot be reached. GitlabCreateError: If the server fails to perform the request. """ url = "/projects/%s/share" % self.id data = {'group_id': group_id, 'group_access': group_access} r = self.gitlab._raw_post(url, data=data, **kwargs) raise_error_from_response(r, GitlabCreateError, 201) def trigger_build(self, ref, token, variables={}, **kwargs): """Trigger a CI build. See https://gitlab.com/help/ci/triggers/README.md#trigger-a-build Args: ref (str): Commit to build; can be a commit SHA, a branch name, ... token (str): The trigger token variables (dict): Variables passed to the build script Raises: GitlabConnectionError: If the server cannot be reached. GitlabCreateError: If the server fails to perform the request. """ url = "/projects/%s/trigger/builds" % self.id form = {r'variables[%s]' % k: v for k, v in six.iteritems(variables)} data = {'ref': ref, 'token': token} data.update(form) r = self.gitlab._raw_post(url, data=data, **kwargs) raise_error_from_response(r, GitlabCreateError, 201) # see #56 - add file attachment features def upload(self, filename, filedata=None, filepath=None, **kwargs): """Upload the specified file into the project. .. note:: Either ``filedata`` or ``filepath`` *MUST* be specified. Args: filename (str): The name of the file being uploaded filedata (bytes): The raw data of the file being uploaded filepath (str): The path to a local file to upload (optional) Raises: GitlabConnectionError: If the server cannot be reached GitlabUploadError: If the file upload fails GitlabUploadError: If ``filedata`` and ``filepath`` are not specified GitlabUploadError: If both ``filedata`` and ``filepath`` are specified Returns: dict: A ``dict`` with the keys: * ``alt`` - The alternate text for the upload * ``url`` - The direct url to the uploaded file * ``markdown`` - Markdown for the uploaded file """ if filepath is None and filedata is None: raise GitlabUploadError("No file contents or path specified") if filedata is not None and filepath is not None: raise GitlabUploadError("File contents and file path specified") if filepath is not None: with open(filepath, "rb") as f: filedata = f.read() url = ("/projects/%(id)s/uploads" % { "id": self.id, }) r = self.gitlab._raw_post( url, files={"file": (filename, filedata)}, ) # returns 201 status code (created) raise_error_from_response(r, GitlabUploadError, expected_code=201) data = r.json() return { "alt": data['alt'], "url": data['url'], "markdown": data['markdown'] } class Runner(GitlabObject): _url = '/runners' canCreate = False optionalUpdateAttrs = ['description', 'active', 'tag_list'] optionalListAttrs = ['scope'] class RunnerManager(BaseManager): obj_cls = Runner def all(self, scope=None, **kwargs): """List all the runners. Args: scope (str): The scope of runners to show, one of: specific, shared, active, paused, online Returns: list(Runner): a list of runners matching the scope. Raises: GitlabConnectionError: If the server cannot be reached. GitlabListError: If the resource cannot be found """ url = '/runners/all' if scope is not None: url += '?scope=' + scope return self.gitlab._raw_list(url, self.obj_cls, **kwargs) class TeamMember(GitlabObject): _url = '/user_teams/%(team_id)s/members' canUpdate = False requiredUrlAttrs = ['teamd_id'] requiredCreateAttrs = ['access_level'] shortPrintAttr = 'username' class Todo(GitlabObject): _url = '/todos' canGet = 'from_list' canUpdate = False canCreate = False optionalListAttrs = ['action', 'author_id', 'project_id', 'state', 'type'] class TodoManager(BaseManager): obj_cls = Todo def delete_all(self, **kwargs): """Mark all the todos as done. Raises: GitlabConnectionError: If the server cannot be reached. GitlabDeleteError: If the resource cannot be found Returns: The number of todos maked done. """ url = '/todos' r = self.gitlab._raw_delete(url, **kwargs) raise_error_from_response(r, GitlabDeleteError) return int(r.text) class ProjectManager(BaseManager): obj_cls = Project def search(self, query, **kwargs): """Search projects by name. API v3 only. .. note:: The search is only performed on the project name (not on the namespace or the description). To perform a smarter search, use the ``search`` argument of the ``list()`` method: .. code-block:: python gl.projects.list(search=your_search_string) Args: query (str): The query string to send to GitLab for the search. all (bool): If True, return all the items, without pagination **kwargs: Additional arguments to send to GitLab. Returns: list(gitlab.Gitlab.Project): A list of matching projects. """ if self.gitlab.api_version == '4': raise NotImplementedError("Not supported by v4 API") return self.gitlab._raw_list("/projects/search/" + query, Project, **kwargs) def all(self, **kwargs): """List all the projects (need admin rights). Args: all (bool): If True, return all the items, without pagination **kwargs: Additional arguments to send to GitLab. Returns: list(gitlab.Gitlab.Project): The list of projects. """ return self.gitlab._raw_list("/projects/all", Project, **kwargs) def owned(self, **kwargs): """List owned projects. Args: all (bool): If True, return all the items, without pagination **kwargs: Additional arguments to send to GitLab. Returns: list(gitlab.Gitlab.Project): The list of owned projects. """ return self.gitlab._raw_list("/projects/owned", Project, **kwargs) def starred(self, **kwargs): """List starred projects. Args: all (bool): If True, return all the items, without pagination **kwargs: Additional arguments to send to GitLab. Returns: list(gitlab.Gitlab.Project): The list of starred projects. """ return self.gitlab._raw_list("/projects/starred", Project, **kwargs) class GroupProject(Project): _url = '/groups/%(group_id)s/projects' canGet = 'from_list' canCreate = False canDelete = False canUpdate = False optionalListAttrs = ['archived', 'visibility', 'order_by', 'sort', 'search', 'ci_enabled_first'] def __init__(self, *args, **kwargs): Project.__init__(self, *args, **kwargs) class GroupProjectManager(ProjectManager): obj_cls = GroupProject class Group(GitlabObject): _url = '/groups' requiredCreateAttrs = ['name', 'path'] optionalCreateAttrs = ['description', 'visibility_level', 'parent_id', 'lfs_enabled', 'request_access_enabled'] optionalUpdateAttrs = ['name', 'path', 'description', 'visibility_level', 'lfs_enabled', 'request_access_enabled'] shortPrintAttr = 'name' managers = ( ('accessrequests', 'GroupAccessRequestManager', [('group_id', 'id')]), ('members', 'GroupMemberManager', [('group_id', 'id')]), ('notificationsettings', 'GroupNotificationSettingsManager', [('group_id', 'id')]), ('projects', 'GroupProjectManager', [('group_id', 'id')]), ('issues', 'GroupIssueManager', [('group_id', 'id')]), ) GUEST_ACCESS = gitlab.GUEST_ACCESS REPORTER_ACCESS = gitlab.REPORTER_ACCESS DEVELOPER_ACCESS = gitlab.DEVELOPER_ACCESS MASTER_ACCESS = gitlab.MASTER_ACCESS OWNER_ACCESS = gitlab.OWNER_ACCESS VISIBILITY_PRIVATE = gitlab.VISIBILITY_PRIVATE VISIBILITY_INTERNAL = gitlab.VISIBILITY_INTERNAL VISIBILITY_PUBLIC = gitlab.VISIBILITY_PUBLIC def transfer_project(self, id, **kwargs): """Transfers a project to this new groups. Args: id (int): ID of the project to transfer. Raises: GitlabConnectionError: If the server cannot be reached. GitlabTransferProjectError: If the server fails to perform the request. """ url = '/groups/%d/projects/%d' % (self.id, id) r = self.gitlab._raw_post(url, None, **kwargs) raise_error_from_response(r, GitlabTransferProjectError, 201) class GroupManager(BaseManager): obj_cls = Group def search(self, query, **kwargs): """Searches groups by name. Args: query (str): The search string all (bool): If True, return all the items, without pagination Returns: list(Group): a list of matching groups. """ url = '/groups?search=' + query return self.gitlab._raw_list(url, self.obj_cls, **kwargs) class TeamMemberManager(BaseManager): obj_cls = TeamMember class TeamProject(GitlabObject): _url = '/user_teams/%(team_id)s/projects' _constructorTypes = {'owner': 'User', 'namespace': 'Group'} canUpdate = False requiredCreateAttrs = ['greatest_access_level'] requiredUrlAttrs = ['team_id'] shortPrintAttr = 'name' class TeamProjectManager(BaseManager): obj_cls = TeamProject class Team(GitlabObject): _url = '/user_teams' shortPrintAttr = 'name' requiredCreateAttrs = ['name', 'path'] canUpdate = False managers = ( ('members', 'TeamMemberManager', [('team_id', 'id')]), ('projects', 'TeamProjectManager', [('team_id', 'id')]), ) class TeamManager(BaseManager): obj_cls = Team python-gitlab-1.3.0/gitlab/v4/000077500000000000000000000000001324224150200160625ustar00rootroot00000000000000python-gitlab-1.3.0/gitlab/v4/__init__.py000066400000000000000000000000001324224150200201610ustar00rootroot00000000000000python-gitlab-1.3.0/gitlab/v4/cli.py000066400000000000000000000316031324224150200172060ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright (C) 2013-2017 Gauvain Pocentek # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . from __future__ import print_function import inspect import operator import six import gitlab import gitlab.base from gitlab import cli import gitlab.v4.objects class GitlabCLI(object): def __init__(self, gl, what, action, args): self.cls_name = cli.what_to_cls(what) self.cls = gitlab.v4.objects.__dict__[self.cls_name] self.what = what.replace('-', '_') self.action = action.lower() self.gl = gl self.args = args self.mgr_cls = getattr(gitlab.v4.objects, self.cls.__name__ + 'Manager') # We could do something smart, like splitting the manager name to find # parents, build the chain of managers to get to the final object. # Instead we do something ugly and efficient: interpolate variables in # the class _path attribute, and replace the value with the result. self.mgr_cls._path = self.mgr_cls._path % self.args self.mgr = self.mgr_cls(gl) def __call__(self): method = 'do_%s' % self.action if hasattr(self, method): return getattr(self, method)() else: return self.do_custom() def do_custom(self): in_obj = cli.custom_actions[self.cls_name][self.action][2] # Get the object (lazy), then act if in_obj: data = {} if hasattr(self.mgr, '_from_parent_attrs'): for k in self.mgr._from_parent_attrs: data[k] = self.args[k] if gitlab.mixins.GetWithoutIdMixin not in inspect.getmro(self.cls): data[self.cls._id_attr] = self.args.pop(self.cls._id_attr) o = self.cls(self.mgr, data) method_name = self.action.replace('-', '_') return getattr(o, method_name)(**self.args) else: return getattr(self.mgr, self.action)(**self.args) def do_create(self): try: return self.mgr.create(self.args) except Exception as e: cli.die("Impossible to create object", e) def do_list(self): try: return self.mgr.list(**self.args) except Exception as e: cli.die("Impossible to list objects", e) def do_get(self): id = None if gitlab.mixins.GetWithoutIdMixin not in inspect.getmro(self.mgr_cls): id = self.args.pop(self.cls._id_attr) try: return self.mgr.get(id, **self.args) except Exception as e: cli.die("Impossible to get object", e) def do_delete(self): id = self.args.pop(self.cls._id_attr) try: self.mgr.delete(id, **self.args) except Exception as e: cli.die("Impossible to destroy object", e) def do_update(self): id = None if gitlab.mixins.GetWithoutIdMixin not in inspect.getmro(self.mgr_cls): id = self.args.pop(self.cls._id_attr) try: return self.mgr.update(id, self.args) except Exception as e: cli.die("Impossible to update object", e) def _populate_sub_parser_by_class(cls, sub_parser): mgr_cls_name = cls.__name__ + 'Manager' mgr_cls = getattr(gitlab.v4.objects, mgr_cls_name) for action_name in ['list', 'get', 'create', 'update', 'delete']: if not hasattr(mgr_cls, action_name): continue sub_parser_action = sub_parser.add_parser(action_name) sub_parser_action.add_argument("--sudo", required=False) if hasattr(mgr_cls, '_from_parent_attrs'): [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), required=True) for x in mgr_cls._from_parent_attrs] if action_name == "list": if hasattr(mgr_cls, '_list_filters'): [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), required=False) for x in mgr_cls._list_filters] sub_parser_action.add_argument("--page", required=False) sub_parser_action.add_argument("--per-page", required=False) sub_parser_action.add_argument("--all", required=False, action='store_true') if action_name == 'delete': id_attr = cls._id_attr.replace('_', '-') sub_parser_action.add_argument("--%s" % id_attr, required=True) if action_name == "get": if gitlab.mixins.GetWithoutIdMixin not in inspect.getmro(cls): if cls._id_attr is not None: id_attr = cls._id_attr.replace('_', '-') sub_parser_action.add_argument("--%s" % id_attr, required=True) if hasattr(mgr_cls, '_optional_get_attrs'): [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), required=False) for x in mgr_cls._optional_get_attrs] if action_name == "create": if hasattr(mgr_cls, '_create_attrs'): [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), required=True) for x in mgr_cls._create_attrs[0] if x != cls._id_attr] [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), required=False) for x in mgr_cls._create_attrs[1] if x != cls._id_attr] if action_name == "update": if cls._id_attr is not None: id_attr = cls._id_attr.replace('_', '-') sub_parser_action.add_argument("--%s" % id_attr, required=True) if hasattr(mgr_cls, '_update_attrs'): [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), required=True) for x in mgr_cls._update_attrs[0] if x != cls._id_attr] [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), required=False) for x in mgr_cls._update_attrs[1] if x != cls._id_attr] if cls.__name__ in cli.custom_actions: name = cls.__name__ for action_name in cli.custom_actions[name]: sub_parser_action = sub_parser.add_parser(action_name) # Get the attributes for URL/path construction if hasattr(mgr_cls, '_from_parent_attrs'): [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), required=True) for x in mgr_cls._from_parent_attrs] sub_parser_action.add_argument("--sudo", required=False) # We need to get the object somehow if gitlab.mixins.GetWithoutIdMixin not in inspect.getmro(cls): if cls._id_attr is not None: id_attr = cls._id_attr.replace('_', '-') sub_parser_action.add_argument("--%s" % id_attr, required=True) required, optional, dummy = cli.custom_actions[name][action_name] [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), required=True) for x in required if x != cls._id_attr] [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), required=False) for x in optional if x != cls._id_attr] if mgr_cls.__name__ in cli.custom_actions: name = mgr_cls.__name__ for action_name in cli.custom_actions[name]: sub_parser_action = sub_parser.add_parser(action_name) if hasattr(mgr_cls, '_from_parent_attrs'): [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), required=True) for x in mgr_cls._from_parent_attrs] sub_parser_action.add_argument("--sudo", required=False) required, optional, dummy = cli.custom_actions[name][action_name] [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), required=True) for x in required if x != cls._id_attr] [sub_parser_action.add_argument("--%s" % x.replace('_', '-'), required=False) for x in optional if x != cls._id_attr] def extend_parser(parser): subparsers = parser.add_subparsers(title='object', dest='what', help="Object to manipulate.") subparsers.required = True # populate argparse for all Gitlab Object classes = [] for cls in gitlab.v4.objects.__dict__.values(): try: if gitlab.base.RESTManager in inspect.getmro(cls): if cls._obj_cls is not None: classes.append(cls._obj_cls) except AttributeError: pass classes.sort(key=operator.attrgetter("__name__")) for cls in classes: arg_name = cli.cls_to_what(cls) object_group = subparsers.add_parser(arg_name) object_subparsers = object_group.add_subparsers( dest='action', help="Action to execute.") _populate_sub_parser_by_class(cls, object_subparsers) object_subparsers.required = True return parser class JSONPrinter(object): def display(self, d, **kwargs): import json # noqa print(json.dumps(d)) class YAMLPrinter(object): def display(self, d, **kwargs): import yaml # noqa print(yaml.safe_dump(d, default_flow_style=False)) class LegacyPrinter(object): def display(self, d, **kwargs): verbose = kwargs.get('verbose', False) padding = kwargs.get('padding', 0) obj = kwargs.get('obj') def display_dict(d, padding): for k in sorted(d.keys()): v = d[k] if isinstance(v, dict): print('%s%s:' % (' ' * padding, k.replace('_', '-'))) new_padding = padding + 2 self.display(v, verbose=True, padding=new_padding, obj=v) continue print('%s%s: %s' % (' ' * padding, k.replace('_', '-'), v)) if verbose: if isinstance(obj, dict): display_dict(obj, padding) return # not a dict, we assume it's a RESTObject if obj._id_attr: id = getattr(obj, obj._id_attr, None) print('%s: %s' % (obj._id_attr, id)) attrs = obj.attributes if obj._id_attr: attrs.pop(obj._id_attr) display_dict(attrs, padding) else: if obj._id_attr: id = getattr(obj, obj._id_attr) print('%s: %s' % (obj._id_attr.replace('_', '-'), id)) if hasattr(obj, '_short_print_attr'): value = getattr(obj, obj._short_print_attr) print('%s: %s' % (obj._short_print_attr, value)) PRINTERS = { 'json': JSONPrinter, 'legacy': LegacyPrinter, 'yaml': YAMLPrinter, } def run(gl, what, action, args, verbose, output, fields): g_cli = GitlabCLI(gl, what, action, args) ret_val = g_cli() printer = PRINTERS[output]() def get_dict(obj): if fields: return {k: v for k, v in obj.attributes.items() if k in fields} return obj.attributes if isinstance(ret_val, dict): printer.display(ret_val, verbose=True, obj=ret_val) elif isinstance(ret_val, list): for obj in ret_val: if isinstance(obj, gitlab.base.RESTObject): printer.display(get_dict(obj), verbose=verbose, obj=obj) else: print(obj) print('') elif isinstance(ret_val, dict): printer.display(ret_val, verbose=verbose, obj=ret_val) elif isinstance(ret_val, gitlab.base.RESTObject): printer.display(get_dict(ret_val), verbose=verbose, obj=ret_val) elif isinstance(ret_val, six.string_types): print(ret_val) python-gitlab-1.3.0/gitlab/v4/objects.py000066400000000000000000003153561324224150200201020ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Copyright (C) 2013-2017 Gauvain Pocentek # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . from __future__ import print_function from __future__ import absolute_import import base64 from gitlab.base import * # noqa from gitlab import cli from gitlab.exceptions import * # noqa from gitlab.mixins import * # noqa from gitlab import utils VISIBILITY_PRIVATE = 'private' VISIBILITY_INTERNAL = 'internal' VISIBILITY_PUBLIC = 'public' ACCESS_GUEST = 10 ACCESS_REPORTER = 20 ACCESS_DEVELOPER = 30 ACCESS_MASTER = 40 ACCESS_OWNER = 50 class SidekiqManager(RESTManager): """Manager for the Sidekiq methods. This manager doesn't actually manage objects but provides helper fonction for the sidekiq metrics API. """ @cli.register_custom_action('SidekiqManager') @exc.on_http_error(exc.GitlabGetError) def queue_metrics(self, **kwargs): """Return the registred queues information. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabGetError: If the information couldn't be retrieved Returns: dict: Information about the Sidekiq queues """ return self.gitlab.http_get('/sidekiq/queue_metrics', **kwargs) @cli.register_custom_action('SidekiqManager') @exc.on_http_error(exc.GitlabGetError) def process_metrics(self, **kwargs): """Return the registred sidekiq workers. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabGetError: If the information couldn't be retrieved Returns: dict: Information about the register Sidekiq worker """ return self.gitlab.http_get('/sidekiq/process_metrics', **kwargs) @cli.register_custom_action('SidekiqManager') @exc.on_http_error(exc.GitlabGetError) def job_stats(self, **kwargs): """Return statistics about the jobs performed. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabGetError: If the information couldn't be retrieved Returns: dict: Statistics about the Sidekiq jobs performed """ return self.gitlab.http_get('/sidekiq/job_stats', **kwargs) @cli.register_custom_action('SidekiqManager') @exc.on_http_error(exc.GitlabGetError) def compound_metrics(self, **kwargs): """Return all available metrics and statistics. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabGetError: If the information couldn't be retrieved Returns: dict: All available Sidekiq metrics and statistics """ 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' class UserActivitiesManager(ListMixin, RESTManager): _path = '/user/activities' _obj_cls = UserActivities class UserCustomAttribute(ObjectDeleteMixin, RESTObject): _id_attr = 'key' class UserCustomAttributeManager(RetrieveMixin, SetMixin, DeleteMixin, RESTManager): _path = '/users/%(user_id)s/custom_attributes' _obj_cls = UserCustomAttribute _from_parent_attrs = {'user_id': 'id'} class UserEmail(ObjectDeleteMixin, RESTObject): _short_print_attr = 'email' class UserEmailManager(RetrieveMixin, CreateMixin, DeleteMixin, RESTManager): _path = '/users/%(user_id)s/emails' _obj_cls = UserEmail _from_parent_attrs = {'user_id': 'id'} _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 class UserGPGKeyManager(RetrieveMixin, CreateMixin, DeleteMixin, RESTManager): _path = '/users/%(user_id)s/gpg_keys' _obj_cls = UserGPGKey _from_parent_attrs = {'user_id': 'id'} _create_attrs = (('key',), tuple()) class UserKey(ObjectDeleteMixin, RESTObject): pass class UserKeyManager(GetFromListMixin, CreateMixin, DeleteMixin, RESTManager): _path = '/users/%(user_id)s/keys' _obj_cls = UserKey _from_parent_attrs = {'user_id': 'id'} _create_attrs = (('title', 'key'), tuple()) class UserImpersonationToken(ObjectDeleteMixin, RESTObject): pass class UserImpersonationTokenManager(NoUpdateMixin, RESTManager): _path = '/users/%(user_id)s/impersonation_tokens' _obj_cls = UserImpersonationToken _from_parent_attrs = {'user_id': 'id'} _create_attrs = (('name', 'scopes'), ('expires_at',)) _list_filters = ('state',) class UserProject(RESTObject): pass class UserProjectManager(ListMixin, CreateMixin, RESTManager): _path = '/projects/user/%(user_id)s' _obj_cls = UserProject _from_parent_attrs = {'user_id': 'id'} _create_attrs = ( ('name', ), ('default_branch', 'issues_enabled', 'wall_enabled', 'merge_requests_enabled', 'wiki_enabled', 'snippets_enabled', 'public', 'visibility', 'description', 'builds_enabled', 'public_builds', 'import_url', 'only_allow_merge_if_build_succeeds') ) _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): _short_print_attr = 'username' _managers = ( ('customattributes', 'UserCustomAttributeManager'), ('emails', 'UserEmailManager'), ('events', 'UserEventManager'), ('gpgkeys', 'UserGPGKeyManager'), ('impersonationtokens', 'UserImpersonationTokenManager'), ('keys', 'UserKeyManager'), ('projects', 'UserProjectManager'), ) @cli.register_custom_action('User') @exc.on_http_error(exc.GitlabBlockError) def block(self, **kwargs): """Block the user. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabBlockError: If the user could not be blocked Returns: bool: Whether the user status has been changed """ path = '/users/%s/block' % self.id server_data = self.manager.gitlab.http_post(path, **kwargs) if server_data is True: self._attrs['state'] = 'blocked' return server_data @cli.register_custom_action('User') @exc.on_http_error(exc.GitlabUnblockError) def unblock(self, **kwargs): """Unblock the user. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabUnblockError: If the user could not be unblocked Returns: bool: Whether the user status has been changed """ path = '/users/%s/unblock' % self.id server_data = self.manager.gitlab.http_post(path, **kwargs) if server_data is True: self._attrs['state'] = 'active' return server_data class UserManager(CRUDMixin, RESTManager): _path = '/users' _obj_cls = User _list_filters = ('active', 'blocked', 'username', 'extern_uid', 'provider', 'external', 'search', 'custom_attributes') _create_attrs = ( tuple(), ('email', 'username', 'name', 'password', 'reset_password', 'skype', 'linkedin', 'twitter', 'projects_limit', 'extern_uid', 'provider', 'bio', 'admin', 'can_create_group', 'website_url', 'skip_confirmation', 'external', 'organization', 'location') ) _update_attrs = ( ('email', 'username', 'name'), ('password', 'skype', 'linkedin', 'twitter', 'projects_limit', 'extern_uid', 'provider', 'bio', 'admin', 'can_create_group', 'website_url', 'skip_confirmation', 'external', 'organization', 'location') ) def _sanitize_data(self, data, action): new_data = data.copy() if 'confirm' in data: new_data['confirm'] = str(new_data['confirm']).lower() return new_data class CurrentUserEmail(ObjectDeleteMixin, RESTObject): _short_print_attr = 'email' class CurrentUserEmailManager(RetrieveMixin, CreateMixin, DeleteMixin, RESTManager): _path = '/user/emails' _obj_cls = CurrentUserEmail _create_attrs = (('email', ), tuple()) class CurrentUserGPGKey(ObjectDeleteMixin, RESTObject): pass class CurrentUserGPGKeyManager(RetrieveMixin, CreateMixin, DeleteMixin, RESTManager): _path = '/user/gpg_keys' _obj_cls = CurrentUserGPGKey _create_attrs = (('key',), tuple()) class CurrentUserKey(ObjectDeleteMixin, RESTObject): _short_print_attr = 'title' class CurrentUserKeyManager(RetrieveMixin, CreateMixin, DeleteMixin, RESTManager): _path = '/user/keys' _obj_cls = CurrentUserKey _create_attrs = (('title', 'key'), tuple()) class CurrentUser(RESTObject): _id_attr = None _short_print_attr = 'username' _managers = ( ('emails', 'CurrentUserEmailManager'), ('gpgkeys', 'CurrentUserGPGKeyManager'), ('keys', 'CurrentUserKeyManager'), ) class CurrentUserManager(GetWithoutIdMixin, RESTManager): _path = '/user' _obj_cls = CurrentUser class ApplicationSettings(SaveMixin, RESTObject): _id_attr = None class ApplicationSettingsManager(GetWithoutIdMixin, UpdateMixin, RESTManager): _path = '/application/settings' _obj_cls = ApplicationSettings _update_attrs = ( tuple(), ('after_sign_out_path', 'container_registry_token_expire_delay', 'default_branch_protection', 'default_project_visibility', 'default_projects_limit', 'default_snippet_visibility', 'domain_blacklist', 'domain_blacklist_enabled', 'domain_whitelist', 'enabled_git_access_protocol', 'gravatar_enabled', 'home_page_url', 'max_attachment_size', 'repository_storage', 'restricted_signup_domains', 'restricted_visibility_levels', 'session_expire_delay', 'sign_in_text', 'signin_enabled', 'signup_enabled', 'twitter_sharing_enabled', 'user_oauth_applications') ) def _sanitize_data(self, data, action): new_data = data.copy() if 'domain_whitelist' in data and data['domain_whitelist'] is None: new_data.pop('domain_whitelist') return new_data class BroadcastMessage(SaveMixin, ObjectDeleteMixin, RESTObject): pass class BroadcastMessageManager(CRUDMixin, RESTManager): _path = '/broadcast_messages' _obj_cls = BroadcastMessage _create_attrs = (('message', ), ('starts_at', 'ends_at', 'color', 'font')) _update_attrs = (tuple(), ('message', 'starts_at', 'ends_at', 'color', 'font')) class DeployKey(RESTObject): pass class DeployKeyManager(GetFromListMixin, RESTManager): _path = '/deploy_keys' _obj_cls = DeployKey class NotificationSettings(SaveMixin, RESTObject): _id_attr = None class NotificationSettingsManager(GetWithoutIdMixin, UpdateMixin, RESTManager): _path = '/notification_settings' _obj_cls = NotificationSettings _update_attrs = ( tuple(), ('level', 'notification_email', 'new_note', 'new_issue', 'reopen_issue', 'close_issue', 'reassign_issue', 'new_merge_request', 'reopen_merge_request', 'close_merge_request', 'reassign_merge_request', 'merge_merge_request') ) class Dockerfile(RESTObject): _id_attr = 'name' class DockerfileManager(RetrieveMixin, RESTManager): _path = '/templates/dockerfiles' _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' class GitignoreManager(RetrieveMixin, RESTManager): _path = '/templates/gitignores' _obj_cls = Gitignore class Gitlabciyml(RESTObject): _id_attr = 'name' class GitlabciymlManager(RetrieveMixin, RESTManager): _path = '/templates/gitlab_ci_ymls' _obj_cls = Gitlabciyml class GroupAccessRequest(AccessRequestMixin, ObjectDeleteMixin, RESTObject): pass class GroupAccessRequestManager(GetFromListMixin, CreateMixin, DeleteMixin, RESTManager): _path = '/groups/%(group_id)s/access_requests' _obj_cls = GroupAccessRequest _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 class GroupIssueManager(GetFromListMixin, RESTManager): _path = '/groups/%(group_id)s/issues' _obj_cls = GroupIssue _from_parent_attrs = {'group_id': 'id'} _list_filters = ('state', 'labels', 'milestone', 'order_by', 'sort') class GroupMember(SaveMixin, ObjectDeleteMixin, RESTObject): _short_print_attr = 'username' class GroupMemberManager(GetFromListMixin, CreateMixin, UpdateMixin, DeleteMixin, RESTManager): _path = '/groups/%(group_id)s/members' _obj_cls = GroupMember _from_parent_attrs = {'group_id': 'id'} _create_attrs = (('access_level', 'user_id'), ('expires_at', )) _update_attrs = (('access_level', ), ('expires_at', )) class GroupMergeRequest(RESTObject): pass class GroupMergeRequestManager(RESTManager): pass class GroupMilestone(SaveMixin, ObjectDeleteMixin, RESTObject): _short_print_attr = 'title' @cli.register_custom_action('GroupMilestone') @exc.on_http_error(exc.GitlabListError) def issues(self, **kwargs): """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: GitlabAuthenticationError: If authentication is not correct GitlabListError: If the list could not be retrieved Returns: RESTObjectList: The list of issues """ path = '%s/%s/issues' % (self.manager.path, self.get_id()) data_list = self.manager.gitlab.http_list(path, as_list=False, **kwargs) manager = GroupIssueManager(self.manager.gitlab, parent=self.manager._parent) # FIXME(gpocentek): the computed manager path is not correct return RESTObjectList(manager, GroupIssue, data_list) @cli.register_custom_action('GroupMilestone') @exc.on_http_error(exc.GitlabListError) def merge_requests(self, **kwargs): """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: GitlabAuthenticationError: If authentication is not correct GitlabListError: If the list could not be retrieved Returns: RESTObjectList: The list of merge requests """ path = '%s/%s/merge_requests' % (self.manager.path, self.get_id()) data_list = self.manager.gitlab.http_list(path, as_list=False, **kwargs) manager = GroupIssueManager(self.manager.gitlab, parent=self.manager._parent) # FIXME(gpocentek): the computed manager path is not correct return RESTObjectList(manager, GroupMergeRequest, data_list) class GroupMilestoneManager(CRUDMixin, RESTManager): _path = '/groups/%(group_id)s/milestones' _obj_cls = GroupMilestone _from_parent_attrs = {'group_id': 'id'} _create_attrs = (('title', ), ('description', 'due_date', 'start_date')) _update_attrs = (tuple(), ('title', 'description', 'due_date', 'start_date', 'state_event')) _list_filters = ('iids', 'state', 'search') class GroupNotificationSettings(NotificationSettings): pass class GroupNotificationSettingsManager(NotificationSettingsManager): _path = '/groups/%(group_id)s/notification_settings' _obj_cls = GroupNotificationSettings _from_parent_attrs = {'group_id': 'id'} class GroupProject(RESTObject): pass class GroupProjectManager(GetFromListMixin, RESTManager): _path = '/groups/%(group_id)s/projects' _obj_cls = GroupProject _from_parent_attrs = {'group_id': 'id'} _list_filters = ('archived', 'visibility', 'order_by', 'sort', 'search', 'ci_enabled_first') class 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' class GroupVariableManager(CRUDMixin, RESTManager): _path = '/groups/%(group_id)s/variables' _obj_cls = GroupVariable _from_parent_attrs = {'group_id': 'id'} _create_attrs = (('key', 'value'), ('protected',)) _update_attrs = (('key', 'value'), ('protected',)) class Group(SaveMixin, ObjectDeleteMixin, RESTObject): _short_print_attr = 'name' _managers = ( ('accessrequests', 'GroupAccessRequestManager'), ('customattributes', 'GroupCustomAttributeManager'), ('issues', 'GroupIssueManager'), ('members', 'GroupMemberManager'), ('milestones', 'GroupMilestoneManager'), ('notificationsettings', 'GroupNotificationSettingsManager'), ('projects', 'GroupProjectManager'), ('subgroups', 'GroupSubgroupManager'), ('variables', 'GroupVariableManager'), ) @cli.register_custom_action('Group', ('to_project_id', )) @exc.on_http_error(exc.GitlabTransferProjectError) def transfer_project(self, to_project_id, **kwargs): """Transfer a project to this group. Args: to_project_id (int): ID of the project to transfer **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabTransferProjectError: If the project could not be transfered """ path = '/groups/%d/projects/%d' % (self.id, to_project_id) self.manager.gitlab.http_post(path, **kwargs) 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', 'request_access_enabled') ) _update_attrs = ( tuple(), ('name', 'path', 'description', 'visibility', 'lfs_enabled', 'request_access_enabled') ) class Hook(ObjectDeleteMixin, RESTObject): _url = '/hooks' _short_print_attr = 'url' class HookManager(NoUpdateMixin, RESTManager): _path = '/hooks' _obj_cls = Hook _create_attrs = (('url', ), tuple()) class Issue(RESTObject): _url = '/issues' _short_print_attr = 'title' class IssueManager(GetFromListMixin, RESTManager): _path = '/issues' _obj_cls = Issue _list_filters = ('state', 'labels', 'order_by', 'sort') class License(RESTObject): _id_attr = 'key' class LicenseManager(RetrieveMixin, RESTManager): _path = '/templates/licenses' _obj_cls = License _list_filters = ('popular', ) _optional_get_attrs = ('project', 'fullname') class Snippet(SaveMixin, ObjectDeleteMixin, RESTObject): _short_print_attr = 'title' @cli.register_custom_action('Snippet') @exc.on_http_error(exc.GitlabGetError) def content(self, streamed=False, action=None, chunk_size=1024, **kwargs): """Return the content of a snippet. Args: streamed (bool): If True the data will be processed by chunks of `chunk_size` and each chunk is passed to `action` for treatment. action (callable): Callable responsible of dealing with chunk of data chunk_size (int): Size of each chunk **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabGetError: If the content could not be retrieved Returns: str: The snippet content """ path = '/snippets/%s/raw' % self.get_id() result = self.manager.gitlab.http_get(path, streamed=streamed, **kwargs) return utils.response_content(result, streamed, action, chunk_size) class SnippetManager(CRUDMixin, RESTManager): _path = '/snippets' _obj_cls = Snippet _create_attrs = (('title', 'file_name', 'content'), ('lifetime', 'visibility')) _update_attrs = (tuple(), ('title', 'file_name', 'content', 'visibility')) @cli.register_custom_action('SnippetManager') def public(self, **kwargs): """List all the public snippets. Args: all (bool): If True the returned object will be a list **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabListError: If the list could not be retrieved Returns: RESTObjectList: A generator for the snippets list """ return self.list(path='/snippets/public', **kwargs) class Namespace(RESTObject): pass class NamespaceManager(GetFromListMixin, RESTManager): _path = '/namespaces' _obj_cls = Namespace _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 class ProjectBoardListManager(CRUDMixin, RESTManager): _path = '/projects/%(project_id)s/boards/%(board_id)s/lists' _obj_cls = ProjectBoardList _from_parent_attrs = {'project_id': 'project_id', 'board_id': 'id'} _create_attrs = (('label_id', ), tuple()) _update_attrs = (('position', ), tuple()) class ProjectBoard(RESTObject): _managers = (('lists', 'ProjectBoardListManager'), ) class ProjectBoardManager(GetFromListMixin, RESTManager): _path = '/projects/%(project_id)s/boards' _obj_cls = ProjectBoard _from_parent_attrs = {'project_id': 'id'} class ProjectBranch(ObjectDeleteMixin, RESTObject): _id_attr = 'name' @cli.register_custom_action('ProjectBranch', tuple(), ('developers_can_push', 'developers_can_merge')) @exc.on_http_error(exc.GitlabProtectError) def protect(self, developers_can_push=False, developers_can_merge=False, **kwargs): """Protect the branch. Args: developers_can_push (bool): Set to True if developers are allowed to push to the branch developers_can_merge (bool): Set to True if developers are allowed to merge to the branch **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabProtectError: If the branch could not be protected """ path = '%s/%s/protect' % (self.manager.path, self.get_id()) post_data = {'developers_can_push': developers_can_push, 'developers_can_merge': developers_can_merge} self.manager.gitlab.http_put(path, post_data=post_data, **kwargs) self._attrs['protected'] = True @cli.register_custom_action('ProjectBranch') @exc.on_http_error(exc.GitlabProtectError) def unprotect(self, **kwargs): """Unprotect the branch. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabProtectError: If the branch could not be unprotected """ path = '%s/%s/unprotect' % (self.manager.path, self.get_id()) self.manager.gitlab.http_put(path, **kwargs) self._attrs['protected'] = False class ProjectBranchManager(NoUpdateMixin, RESTManager): _path = '/projects/%(project_id)s/repository/branches' _obj_cls = ProjectBranch _from_parent_attrs = {'project_id': 'id'} _create_attrs = (('branch', 'ref'), tuple()) class 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) def cancel(self, **kwargs): """Cancel the job. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabJobCancelError: If the job could not be canceled """ path = '%s/%s/cancel' % (self.manager.path, self.get_id()) self.manager.gitlab.http_post(path) @cli.register_custom_action('ProjectJob') @exc.on_http_error(exc.GitlabJobRetryError) def retry(self, **kwargs): """Retry the job. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabJobRetryError: If the job could not be retried """ path = '%s/%s/retry' % (self.manager.path, self.get_id()) self.manager.gitlab.http_post(path) @cli.register_custom_action('ProjectJob') @exc.on_http_error(exc.GitlabJobPlayError) def play(self, **kwargs): """Trigger a job explicitly. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabJobPlayError: If the job could not be triggered """ path = '%s/%s/play' % (self.manager.path, self.get_id()) self.manager.gitlab.http_post(path) @cli.register_custom_action('ProjectJob') @exc.on_http_error(exc.GitlabJobEraseError) def erase(self, **kwargs): """Erase the job (remove job artifacts and trace). Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabJobEraseError: If the job could not be erased """ path = '%s/%s/erase' % (self.manager.path, self.get_id()) self.manager.gitlab.http_post(path) @cli.register_custom_action('ProjectJob') @exc.on_http_error(exc.GitlabCreateError) def keep_artifacts(self, **kwargs): """Prevent artifacts from being deleted when expiration is set. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabCreateError: If the request could not be performed """ path = '%s/%s/artifacts/keep' % (self.manager.path, self.get_id()) self.manager.gitlab.http_post(path) @cli.register_custom_action('ProjectJob') @exc.on_http_error(exc.GitlabGetError) def artifacts(self, streamed=False, action=None, chunk_size=1024, **kwargs): """Get the job artifacts. Args: streamed (bool): If True the data will be processed by chunks of `chunk_size` and each chunk is passed to `action` for treatment action (callable): Callable responsible of dealing with chunk of data chunk_size (int): Size of each chunk **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabGetError: If the artifacts could not be retrieved Returns: str: The artifacts if `streamed` is False, None otherwise. """ path = '%s/%s/artifacts' % (self.manager.path, self.get_id()) result = self.manager.gitlab.http_get(path, streamed=streamed, **kwargs) return utils.response_content(result, streamed, action, chunk_size) @cli.register_custom_action('ProjectJob') @exc.on_http_error(exc.GitlabGetError) def trace(self, streamed=False, action=None, chunk_size=1024, **kwargs): """Get the job trace. Args: streamed (bool): If True the data will be processed by chunks of `chunk_size` and each chunk is passed to `action` for treatment action (callable): Callable responsible of dealing with chunk of data chunk_size (int): Size of each chunk **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabGetError: If the artifacts could not be retrieved Returns: str: The trace """ path = '%s/%s/trace' % (self.manager.path, self.get_id()) result = self.manager.gitlab.http_get(path, streamed=streamed, **kwargs) return utils.response_content(result, streamed, action, chunk_size) class ProjectJobManager(RetrieveMixin, RESTManager): _path = '/projects/%(project_id)s/jobs' _obj_cls = ProjectJob _from_parent_attrs = {'project_id': 'id'} class ProjectCommitStatus(RESTObject): pass class ProjectCommitStatusManager(GetFromListMixin, CreateMixin, RESTManager): _path = ('/projects/%(project_id)s/repository/commits/%(commit_id)s' '/statuses') _obj_cls = ProjectCommitStatus _from_parent_attrs = {'project_id': 'project_id', 'commit_id': 'id'} _create_attrs = (('state', ), ('description', 'name', 'context', 'ref', 'target_url', 'coverage')) def create(self, data, **kwargs): """Create a new object. Args: data (dict): Parameters to send to the server to create the resource **kwargs: Extra data to send to the Gitlab server (e.g. sudo or 'ref_name', 'stage', 'name', 'all'. Raises: GitlabAuthenticationError: If authentication is not correct GitlabCreateError: If the server cannot perform the request Returns: RESTObject: A new instance of the manage object class build with the data sent by the server """ path = '/projects/%(project_id)s/statuses/%(commit_id)s' computed_path = self._compute_path(path) return CreateMixin.create(self, data, path=computed_path, **kwargs) class ProjectCommitComment(RESTObject): _id_attr = None class ProjectCommitCommentManager(ListMixin, CreateMixin, RESTManager): _path = ('/projects/%(project_id)s/repository/commits/%(commit_id)s' '/comments') _obj_cls = ProjectCommitComment _from_parent_attrs = {'project_id': 'project_id', 'commit_id': 'id'} _create_attrs = (('note', ), ('path', 'line', 'line_type')) class ProjectCommit(RESTObject): _short_print_attr = 'title' _managers = ( ('comments', 'ProjectCommitCommentManager'), ('statuses', 'ProjectCommitStatusManager'), ) @cli.register_custom_action('ProjectCommit') @exc.on_http_error(exc.GitlabGetError) def diff(self, **kwargs): """Generate the commit diff. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabGetError: If the diff could not be retrieved Returns: list: The changes done in this commit """ path = '%s/%s/diff' % (self.manager.path, self.get_id()) return self.manager.gitlab.http_get(path, **kwargs) @cli.register_custom_action('ProjectCommit', ('branch',)) @exc.on_http_error(exc.GitlabCherryPickError) def cherry_pick(self, branch, **kwargs): """Cherry-pick a commit into a branch. Args: branch (str): Name of target branch **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabCherryPickError: If the cherry-pick could not be performed """ path = '%s/%s/cherry_pick' % (self.manager.path, self.get_id()) post_data = {'branch': branch} self.manager.gitlab.http_post(path, post_data=post_data, **kwargs) class ProjectCommitManager(RetrieveMixin, CreateMixin, RESTManager): _path = '/projects/%(project_id)s/repository/commits' _obj_cls = ProjectCommit _from_parent_attrs = {'project_id': 'id'} _create_attrs = (('branch', 'commit_message', 'actions'), ('author_email', 'author_name')) class ProjectEnvironment(SaveMixin, ObjectDeleteMixin, RESTObject): pass class ProjectEnvironmentManager(GetFromListMixin, CreateMixin, UpdateMixin, DeleteMixin, RESTManager): _path = '/projects/%(project_id)s/environments' _obj_cls = ProjectEnvironment _from_parent_attrs = {'project_id': 'id'} _create_attrs = (('name', ), ('external_url', )) _update_attrs = (tuple(), ('name', 'external_url')) class ProjectKey(ObjectDeleteMixin, RESTObject): pass class ProjectKeyManager(NoUpdateMixin, RESTManager): _path = '/projects/%(project_id)s/deploy_keys' _obj_cls = ProjectKey _from_parent_attrs = {'project_id': 'id'} _create_attrs = (('title', 'key'), tuple()) @cli.register_custom_action('ProjectKeyManager', ('key_id',)) @exc.on_http_error(exc.GitlabProjectDeployKeyError) def enable(self, key_id, **kwargs): """Enable a deploy key for a project. Args: key_id (int): The ID of the key to enable **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabProjectDeployKeyError: If the key could not be enabled """ path = '%s/%s/enable' % (self.path, key_id) self.gitlab.http_post(path, **kwargs) class ProjectEvent(Event): pass class ProjectEventManager(EventManager): _path = '/projects/%(project_id)s/events' _obj_cls = ProjectEvent _from_parent_attrs = {'project_id': 'id'} class ProjectFork(RESTObject): pass class ProjectForkManager(CreateMixin, RESTManager): _path = '/projects/%(project_id)s/fork' _obj_cls = ProjectFork _from_parent_attrs = {'project_id': 'id'} _create_attrs = (tuple(), ('namespace', )) class ProjectHook(SaveMixin, ObjectDeleteMixin, RESTObject): _short_print_attr = 'url' class ProjectHookManager(CRUDMixin, RESTManager): _path = '/projects/%(project_id)s/hooks' _obj_cls = ProjectHook _from_parent_attrs = {'project_id': 'id'} _create_attrs = ( ('url', ), ('push_events', 'issues_events', 'note_events', 'merge_requests_events', 'tag_push_events', 'build_events', 'enable_ssl_verification', 'token', 'pipeline_events') ) _update_attrs = ( ('url', ), ('push_events', 'issues_events', 'note_events', 'merge_requests_events', 'tag_push_events', 'build_events', 'enable_ssl_verification', 'token', 'pipeline_events') ) 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 _from_parent_attrs = {'project_id': 'project_id', 'issue_iid': 'iid'} _create_attrs = (('body', ), ('created_at', )) _update_attrs = (('body', ), tuple()) class ProjectIssue(SubscribableMixin, TodoMixin, TimeTrackingMixin, SaveMixin, ObjectDeleteMixin, RESTObject): _short_print_attr = 'title' _id_attr = 'iid' _managers = ( ('notes', 'ProjectIssueNoteManager'), ('awardemojis', 'ProjectIssueAwardEmojiManager'), ) @cli.register_custom_action('ProjectIssue') @exc.on_http_error(exc.GitlabUpdateError) def user_agent_detail(self, **kwargs): """Get user agent detail. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabGetError: If the detail could not be retrieved """ path = '%s/%s/user_agent_detail' % (self.manager.path, self.get_id()) return self.manager.gitlab.http_get(path, **kwargs) @cli.register_custom_action('ProjectIssue', ('to_project_id',)) @exc.on_http_error(exc.GitlabUpdateError) def move(self, to_project_id, **kwargs): """Move the issue to another project. Args: to_project_id(int): ID of the target project **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabUpdateError: If the issue could not be moved """ path = '%s/%s/move' % (self.manager.path, self.get_id()) data = {'to_project_id': to_project_id} server_data = self.manager.gitlab.http_post(path, post_data=data, **kwargs) self._update_attrs(server_data) class ProjectIssueManager(CRUDMixin, RESTManager): _path = '/projects/%(project_id)s/issues/' _obj_cls = ProjectIssue _from_parent_attrs = {'project_id': 'id'} _list_filters = ('state', 'labels', 'milestone', 'order_by', 'sort') _create_attrs = (('title', ), ('description', 'assignee_id', 'milestone_id', 'labels', 'created_at', 'due_date')) _update_attrs = (tuple(), ('title', 'description', 'assignee_id', 'milestone_id', 'labels', 'created_at', 'updated_at', 'state_event', 'due_date')) def _sanitize_data(self, data, action): new_data = data.copy() if 'labels' in data: new_data['labels'] = ','.join(data['labels']) return new_data class ProjectMember(SaveMixin, ObjectDeleteMixin, RESTObject): _short_print_attr = 'username' class ProjectMemberManager(CRUDMixin, RESTManager): _path = '/projects/%(project_id)s/members' _obj_cls = ProjectMember _from_parent_attrs = {'project_id': 'id'} _create_attrs = (('access_level', 'user_id'), ('expires_at', )) _update_attrs = (('access_level', ), ('expires_at', )) class ProjectNote(RESTObject): pass class ProjectNoteManager(RetrieveMixin, RESTManager): _path = '/projects/%(project_id)s/notes' _obj_cls = ProjectNote _from_parent_attrs = {'project_id': 'id'} _create_attrs = (('body', ), tuple()) class ProjectNotificationSettings(NotificationSettings): pass class ProjectNotificationSettingsManager(NotificationSettingsManager): _path = '/projects/%(project_id)s/notification_settings' _obj_cls = ProjectNotificationSettings _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' @cli.register_custom_action('ProjectTag', ('description', )) def set_release_description(self, description, **kwargs): """Set the release notes on the tag. If the release doesn't exist yet, it will be created. If it already exists, its description will be updated. Args: description (str): Description of the release. **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabCreateError: If the server fails to create the release GitlabUpdateError: If the server fails to update the release """ id = self.get_id().replace('/', '%2F') path = '%s/%s/release' % (self.manager.path, id) data = {'description': description} if self.release is None: try: server_data = self.manager.gitlab.http_post(path, post_data=data, **kwargs) except exc.GitlabHttpError as e: raise exc.GitlabCreateError(e.response_code, e.error_message) else: try: server_data = self.manager.gitlab.http_put(path, post_data=data, **kwargs) except exc.GitlabHttpError as e: raise exc.GitlabUpdateError(e.response_code, e.error_message) self.release = server_data class ProjectTagManager(NoUpdateMixin, RESTManager): _path = '/projects/%(project_id)s/repository/tags' _obj_cls = ProjectTag _from_parent_attrs = {'project_id': 'id'} _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 class ProjectMergeRequestDiffManager(RetrieveMixin, RESTManager): _path = '/projects/%(project_id)s/merge_requests/%(mr_iid)s/versions' _obj_cls = ProjectMergeRequestDiff _from_parent_attrs = {'project_id': 'project_id', 'mr_iid': 'iid'} 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 _from_parent_attrs = {'project_id': 'project_id', 'mr_iid': 'iid'} _create_attrs = (('body', ), tuple()) _update_attrs = (('body', ), tuple()) class ProjectMergeRequest(SubscribableMixin, TodoMixin, TimeTrackingMixin, SaveMixin, ObjectDeleteMixin, RESTObject): _id_attr = 'iid' _managers = ( ('awardemojis', 'ProjectMergeRequestAwardEmojiManager'), ('diffs', 'ProjectMergeRequestDiffManager'), ('notes', 'ProjectMergeRequestNoteManager'), ) @cli.register_custom_action('ProjectMergeRequest') @exc.on_http_error(exc.GitlabMROnBuildSuccessError) def cancel_merge_when_pipeline_succeeds(self, **kwargs): """Cancel merge when the pipeline succeeds. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabMROnBuildSuccessError: If the server could not handle the request """ path = ('%s/%s/cancel_merge_when_pipeline_succeeds' % (self.manager.path, self.get_id())) server_data = self.manager.gitlab.http_put(path, **kwargs) self._update_attrs(server_data) @cli.register_custom_action('ProjectMergeRequest') @exc.on_http_error(exc.GitlabListError) def closes_issues(self, **kwargs): """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: GitlabAuthenticationError: If authentication is not correct GitlabListError: If the list could not be retrieved Returns: RESTObjectList: List of issues """ path = '%s/%s/closes_issues' % (self.manager.path, self.get_id()) data_list = self.manager.gitlab.http_list(path, as_list=False, **kwargs) manager = ProjectIssueManager(self.manager.gitlab, parent=self.manager._parent) return RESTObjectList(manager, ProjectIssue, data_list) @cli.register_custom_action('ProjectMergeRequest') @exc.on_http_error(exc.GitlabListError) def commits(self, **kwargs): """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: GitlabAuthenticationError: If authentication is not correct GitlabListError: If the list could not be retrieved Returns: RESTObjectList: The list of commits """ path = '%s/%s/commits' % (self.manager.path, self.get_id()) data_list = self.manager.gitlab.http_list(path, as_list=False, **kwargs) manager = ProjectCommitManager(self.manager.gitlab, parent=self.manager._parent) return RESTObjectList(manager, ProjectCommit, data_list) @cli.register_custom_action('ProjectMergeRequest') @exc.on_http_error(exc.GitlabListError) def changes(self, **kwargs): """List the merge request changes. Args: **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: List of changes """ path = '%s/%s/changes' % (self.manager.path, self.get_id()) return self.manager.gitlab.http_get(path, **kwargs) @cli.register_custom_action('ProjectMergeRequest', tuple(), ('merge_commit_message', 'should_remove_source_branch', 'merge_when_pipeline_succeeds')) @exc.on_http_error(exc.GitlabMRClosedError) def merge(self, merge_commit_message=None, should_remove_source_branch=False, merge_when_pipeline_succeeds=False, **kwargs): """Accept the merge request. Args: merge_commit_message (bool): Commit message should_remove_source_branch (bool): If True, removes the source branch merge_when_pipeline_succeeds (bool): Wait for the build to succeed, then merge **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabMRClosedError: If the merge failed """ path = '%s/%s/merge' % (self.manager.path, self.get_id()) data = {} if merge_commit_message: data['merge_commit_message'] = merge_commit_message if should_remove_source_branch: data['should_remove_source_branch'] = True if merge_when_pipeline_succeeds: data['merge_when_pipeline_succeeds'] = True server_data = self.manager.gitlab.http_put(path, post_data=data, **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' _obj_cls = ProjectMergeRequest _from_parent_attrs = {'project_id': 'id'} _create_attrs = ( ('source_branch', 'target_branch', 'title'), ('assignee_id', 'description', 'target_project_id', 'labels', 'milestone_id', 'remove_source_branch') ) _update_attrs = (tuple(), ('target_branch', 'assignee_id', 'title', 'description', 'state_event', 'labels', 'milestone_id')) _list_filters = ('iids', 'state', 'order_by', 'sort') def _sanitize_data(self, data, action): new_data = data.copy() if 'labels' in data: new_data['labels'] = ','.join(data['labels']) return new_data class ProjectMilestone(SaveMixin, ObjectDeleteMixin, RESTObject): _short_print_attr = 'title' @cli.register_custom_action('ProjectMilestone') @exc.on_http_error(exc.GitlabListError) def issues(self, **kwargs): """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: GitlabAuthenticationError: If authentication is not correct GitlabListError: If the list could not be retrieved Returns: RESTObjectList: The list of issues """ path = '%s/%s/issues' % (self.manager.path, self.get_id()) data_list = self.manager.gitlab.http_list(path, as_list=False, **kwargs) manager = ProjectIssueManager(self.manager.gitlab, parent=self.manager._parent) # FIXME(gpocentek): the computed manager path is not correct return RESTObjectList(manager, ProjectIssue, data_list) @cli.register_custom_action('ProjectMilestone') @exc.on_http_error(exc.GitlabListError) def merge_requests(self, **kwargs): """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: GitlabAuthenticationError: If authentication is not correct GitlabListError: If the list could not be retrieved Returns: RESTObjectList: The list of merge requests """ path = '%s/%s/merge_requests' % (self.manager.path, self.get_id()) data_list = self.manager.gitlab.http_list(path, as_list=False, **kwargs) manager = ProjectMergeRequestManager(self.manager.gitlab, parent=self.manager._parent) # FIXME(gpocentek): the computed manager path is not correct return RESTObjectList(manager, ProjectMergeRequest, data_list) class ProjectMilestoneManager(CRUDMixin, RESTManager): _path = '/projects/%(project_id)s/milestones' _obj_cls = ProjectMilestone _from_parent_attrs = {'project_id': 'id'} _create_attrs = (('title', ), ('description', 'due_date', 'start_date', 'state_event')) _update_attrs = (tuple(), ('title', 'description', 'due_date', 'start_date', 'state_event')) _list_filters = ('iids', 'state', 'search') class ProjectLabel(SubscribableMixin, SaveMixin, ObjectDeleteMixin, RESTObject): _id_attr = 'name' # Update without ID, but we need an ID to get from list. @exc.on_http_error(exc.GitlabUpdateError) def save(self, **kwargs): """Saves the changes made to the object to the server. The object is updated to match what the server returns. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct. GitlabUpdateError: If the server cannot perform the request. """ updated_data = self._get_updated_data() # call the manager server_data = self.manager.update(None, updated_data, **kwargs) self._update_attrs(server_data) class ProjectLabelManager(GetFromListMixin, CreateMixin, UpdateMixin, DeleteMixin, RESTManager): _path = '/projects/%(project_id)s/labels' _obj_cls = ProjectLabel _from_parent_attrs = {'project_id': 'id'} _create_attrs = (('name', 'color'), ('description', 'priority')) _update_attrs = (('name', ), ('new_name', 'color', 'description', 'priority')) # Delete without ID. @exc.on_http_error(exc.GitlabDeleteError) def delete(self, name, **kwargs): """Delete a Label on the server. Args: name: The name of the label **kwargs: Extra options to send to the Gitlab server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct. GitlabDeleteError: If the server cannot perform the request. """ self.gitlab.http_delete(self.path, query_data={'name': name}, **kwargs) class ProjectFile(SaveMixin, ObjectDeleteMixin, RESTObject): _id_attr = 'file_path' _short_print_attr = 'file_path' def decode(self): """Returns the decoded content of the file. Returns: (str): the decoded content. """ return base64.b64decode(self.content) def save(self, branch, commit_message, **kwargs): """Save the changes made to the file to the server. The object is updated to match what the server returns. Args: branch (str): Branch in which the file will be updated commit_message (str): Message to send with the commit **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabUpdateError: If the server cannot perform the request """ self.branch = branch self.commit_message = commit_message self.file_path = self.file_path.replace('/', '%2F') super(ProjectFile, self).save(**kwargs) def delete(self, branch, commit_message, **kwargs): """Delete the file from the server. Args: branch (str): Branch from which the file will be removed commit_message (str): Commit message for the deletion **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabDeleteError: If the server cannot perform the request """ file_path = self.get_id().replace('/', '%2F') self.manager.delete(file_path, branch, commit_message, **kwargs) class ProjectFileManager(GetMixin, CreateMixin, UpdateMixin, DeleteMixin, RESTManager): _path = '/projects/%(project_id)s/repository/files' _obj_cls = ProjectFile _from_parent_attrs = {'project_id': 'id'} _create_attrs = (('file_path', 'branch', 'content', 'commit_message'), ('encoding', 'author_email', 'author_name')) _update_attrs = (('file_path', 'branch', 'content', 'commit_message'), ('encoding', 'author_email', 'author_name')) @cli.register_custom_action('ProjectFileManager', ('file_path', 'ref')) def get(self, file_path, ref, **kwargs): """Retrieve a single file. Args: file_path (str): Path of the file to retrieve ref (str): Name of the branch, tag or commit **kwargs: Extra options to send to the Gitlab server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabGetError: If the file could not be retrieved Returns: object: The generated RESTObject """ file_path = file_path.replace('/', '%2F') return GetMixin.get(self, file_path, ref=ref, **kwargs) @cli.register_custom_action('ProjectFileManager', ('file_path', 'branch', 'content', 'commit_message'), ('encoding', 'author_email', 'author_name')) @exc.on_http_error(exc.GitlabCreateError) def create(self, data, **kwargs): """Create a new object. Args: data (dict): parameters to send to the server to create the resource **kwargs: Extra options to send to the Gitlab server (e.g. sudo) Returns: RESTObject: a new instance of the managed object class built with the data sent by the server Raises: GitlabAuthenticationError: If authentication is not correct GitlabCreateError: If the server cannot perform the request """ self._check_missing_create_attrs(data) 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=new_data, **kwargs) return self._obj_cls(self, server_data) @exc.on_http_error(exc.GitlabUpdateError) def update(self, file_path, new_data={}, **kwargs): """Update an object on the server. Args: id: ID of the object to update (can be None if not required) new_data: the update data for the object **kwargs: Extra options to send to the Gitlab server (e.g. sudo) Returns: dict: The new object data (*not* a RESTObject) Raises: GitlabAuthenticationError: If authentication is not correct GitlabUpdateError: If the server cannot perform the request """ data = new_data.copy() file_path = file_path.replace('/', '%2F') data['file_path'] = file_path path = '%s/%s' % (self.path, file_path) self._check_missing_update_attrs(data) return self.gitlab.http_put(path, post_data=data, **kwargs) @cli.register_custom_action('ProjectFileManager', ('file_path', 'branch', 'commit_message')) @exc.on_http_error(exc.GitlabDeleteError) def delete(self, file_path, branch, commit_message, **kwargs): """Delete a file on the server. Args: file_path (str): Path of the file to remove branch (str): Branch from which the file will be removed commit_message (str): Commit message for the deletion **kwargs: Extra options to send to the Gitlab server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabDeleteError: If the server cannot perform the request """ path = '%s/%s' % (self.path, file_path.replace('/', '%2F')) data = {'branch': branch, 'commit_message': commit_message} self.gitlab.http_delete(path, query_data=data, **kwargs) @cli.register_custom_action('ProjectFileManager', ('file_path', 'ref')) @exc.on_http_error(exc.GitlabGetError) def raw(self, file_path, ref, streamed=False, action=None, chunk_size=1024, **kwargs): """Return the content of a file for a commit. Args: ref (str): ID of the commit filepath (str): Path of the file to return streamed (bool): If True the data will be processed by chunks of `chunk_size` and each chunk is passed to `action` for treatment action (callable): Callable responsible of dealing with chunk of data chunk_size (int): Size of each chunk **kwargs: Extra options to send to the Gitlab server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabGetError: If the file could not be retrieved Returns: str: The file content """ file_path = file_path.replace('/', '%2F').replace('.', '%2E') path = '%s/%s/raw' % (self.path, file_path) query_data = {'ref': ref} result = self.gitlab.http_get(path, query_data=query_data, streamed=streamed, **kwargs) return utils.response_content(result, streamed, action, chunk_size) class 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', 'ProjectPipelineJobManager'), ) @cli.register_custom_action('ProjectPipeline') @exc.on_http_error(exc.GitlabPipelineCancelError) def cancel(self, **kwargs): """Cancel the job. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabPipelineCancelError: If the request failed """ path = '%s/%s/cancel' % (self.manager.path, self.get_id()) self.manager.gitlab.http_post(path) @cli.register_custom_action('ProjectPipeline') @exc.on_http_error(exc.GitlabPipelineRetryError) def retry(self, **kwargs): """Retry the job. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabPipelineRetryError: If the request failed """ path = '%s/%s/retry' % (self.manager.path, self.get_id()) self.manager.gitlab.http_post(path) class ProjectPipelineManager(RetrieveMixin, CreateMixin, RESTManager): _path = '/projects/%(project_id)s/pipelines' _obj_cls = ProjectPipeline _from_parent_attrs = {'project_id': 'id'} _create_attrs = (('ref', ), tuple()) 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) 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 """ path = self.path[:-1] # drop the 's' return CreateMixin.create(self, data, path=path, **kwargs) class ProjectPipelineScheduleVariable(SaveMixin, ObjectDeleteMixin, RESTObject): _id_attr = 'key' 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 = (('key', 'value'), tuple()) _update_attrs = (('key', 'value'), tuple()) 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. 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 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')) class ProjectPipelineJob(ProjectJob): pass 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 _from_parent_attrs = {'project_id': 'project_id', 'snippet_id': 'id'} _create_attrs = (('body', ), tuple()) _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 = ( ('awardemojis', 'ProjectSnippetAwardEmojiManager'), ('notes', 'ProjectSnippetNoteManager'), ) @cli.register_custom_action('ProjectSnippet') @exc.on_http_error(exc.GitlabGetError) def content(self, streamed=False, action=None, chunk_size=1024, **kwargs): """Return the content of a snippet. Args: streamed (bool): If True the data will be processed by chunks of `chunk_size` and each chunk is passed to `action` for treatment. action (callable): Callable responsible of dealing with chunk of data chunk_size (int): Size of each chunk **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabGetError: If the content could not be retrieved Returns: str: The snippet content """ path = "%s/%s/raw" % (self.manager.path, self.get_id()) result = self.manager.gitlab.http_get(path, streamed=streamed, **kwargs) return utils.response_content(result, streamed, action, chunk_size) class ProjectSnippetManager(CRUDMixin, RESTManager): _path = '/projects/%(project_id)s/snippets' _obj_cls = ProjectSnippet _from_parent_attrs = {'project_id': 'id'} _create_attrs = (('title', 'file_name', 'code'), ('lifetime', 'visibility')) _update_attrs = (tuple(), ('title', 'file_name', 'code', 'visibility')) class ProjectTrigger(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. 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 ProjectTriggerManager(CRUDMixin, RESTManager): _path = '/projects/%(project_id)s/triggers' _obj_cls = ProjectTrigger _from_parent_attrs = {'project_id': 'id'} _create_attrs = (('description', ), tuple()) _update_attrs = (('description', ), tuple()) class ProjectUser(RESTObject): pass class ProjectUserManager(ListMixin, RESTManager): _path = '/projects/%(project_id)s/users' _obj_cls = ProjectUser _from_parent_attrs = {'project_id': 'id'} _list_filters = ('search',) class ProjectVariable(SaveMixin, ObjectDeleteMixin, RESTObject): _id_attr = 'key' class ProjectVariableManager(CRUDMixin, RESTManager): _path = '/projects/%(project_id)s/variables' _obj_cls = ProjectVariable _from_parent_attrs = {'project_id': 'id'} _create_attrs = (('key', 'value'), tuple()) _update_attrs = (('key', 'value'), tuple()) class ProjectService(SaveMixin, ObjectDeleteMixin, RESTObject): pass class ProjectServiceManager(GetMixin, UpdateMixin, DeleteMixin, RESTManager): _path = '/projects/%(project_id)s/services' _from_parent_attrs = {'project_id': 'id'} _obj_cls = ProjectService _service_attrs = { 'asana': (('api_key', ), ('restrict_to_branch', )), 'assembla': (('token', ), ('subdomain', )), 'bamboo': (('bamboo_url', 'build_key', 'username', 'password'), tuple()), 'buildkite': (('token', 'project_url'), ('enable_ssl_verification', )), 'campfire': (('token', ), ('subdomain', 'room')), 'custom-issue-tracker': (('new_issue_url', 'issues_url', 'project_url'), ('description', 'title')), '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()), 'hipchat': (('token', ), ('color', 'notify', 'room', 'api_version', 'server')), 'irker': (('recipients', ), ('default_irc_uri', 'server_port', 'server_host', 'colorize_messages')), 'jira': (('url', 'project_key'), ('new_issue_url', 'project_url', 'issues_url', 'api_url', 'description', 'username', 'password', 'jira_issue_transition_id')), 'mattermost': (('webhook',), ('username', 'channel')), 'pivotaltracker': (('token', ), tuple()), 'pushover': (('api_key', 'user_key', 'priority'), ('device', 'sound')), 'redmine': (('new_issue_url', 'project_url', 'issues_url'), ('description', )), 'slack': (('webhook', ), ('username', 'channel')), 'teamcity': (('teamcity_url', 'build_type', 'username', 'password'), tuple()) } def get(self, id, **kwargs): """Retrieve a single object. Args: id (int or str): ID of the object to retrieve lazy (bool): If True, don't request the server, but create a shallow object giving access to the managers. This is useful if you want to avoid useless calls to the API. **kwargs: Extra options to send to the Gitlab server (e.g. sudo) Returns: object: The generated RESTObject. Raises: GitlabAuthenticationError: If authentication is not correct GitlabGetError: If the server cannot perform the request """ obj = super(ProjectServiceManager, self).get(id, **kwargs) obj.id = id return obj def update(self, id=None, new_data={}, **kwargs): """Update an object on the server. Args: id: ID of the object to update (can be None if not required) new_data: the update data for the object **kwargs: Extra options to send to the Gitlab server (e.g. sudo) Returns: dict: The new object data (*not* a RESTObject) Raises: GitlabAuthenticationError: If authentication is not correct GitlabUpdateError: If the server cannot perform the request """ super(ProjectServiceManager, self).update(id, new_data, **kwargs) self.id = id @cli.register_custom_action('ProjectServiceManager') def available(self, **kwargs): """List the services known by python-gitlab. Returns: list (str): The list of service code names. """ return list(self._service_attrs.keys()) class ProjectAccessRequest(AccessRequestMixin, ObjectDeleteMixin, RESTObject): pass class ProjectAccessRequestManager(GetFromListMixin, CreateMixin, DeleteMixin, RESTManager): _path = '/projects/%(project_id)s/access_requests' _obj_cls = ProjectAccessRequest _from_parent_attrs = {'project_id': 'id'} class ProjectDeployment(RESTObject): pass class ProjectDeploymentManager(RetrieveMixin, RESTManager): _path = '/projects/%(project_id)s/deployments' _obj_cls = ProjectDeployment _from_parent_attrs = {'project_id': 'id'} class ProjectProtectedBranch(ObjectDeleteMixin, RESTObject): _id_attr = 'name' class ProjectProtectedBranchManager(NoUpdateMixin, RESTManager): _path = '/projects/%(project_id)s/protected_branches' _obj_cls = ProjectProtectedBranch _from_parent_attrs = {'project_id': 'id'} _create_attrs = (('name', ), ('push_access_level', 'merge_access_level')) class ProjectRunner(ObjectDeleteMixin, RESTObject): pass class ProjectRunnerManager(NoUpdateMixin, RESTManager): _path = '/projects/%(project_id)s/runners' _obj_cls = ProjectRunner _from_parent_attrs = {'project_id': 'id'} _create_attrs = (('runner_id', ), tuple()) class ProjectWiki(SaveMixin, ObjectDeleteMixin, RESTObject): _id_attr = 'slug' _short_print_attr = 'slug' class ProjectWikiManager(CRUDMixin, RESTManager): _path = '/projects/%(project_id)s/wikis' _obj_cls = ProjectWiki _from_parent_attrs = {'project_id': 'id'} _create_attrs = (('title', 'content'), ('format', )) _update_attrs = (tuple(), ('title', 'content', 'format')) _list_filters = ('with_content', ) class Project(SaveMixin, ObjectDeleteMixin, RESTObject): _short_print_attr = 'path' _managers = ( ('accessrequests', 'ProjectAccessRequestManager'), ('boards', 'ProjectBoardManager'), ('branches', 'ProjectBranchManager'), ('jobs', 'ProjectJobManager'), ('commits', 'ProjectCommitManager'), ('customattributes', 'ProjectCustomAttributeManager'), ('deployments', 'ProjectDeploymentManager'), ('environments', 'ProjectEnvironmentManager'), ('events', 'ProjectEventManager'), ('files', 'ProjectFileManager'), ('forks', 'ProjectForkManager'), ('hooks', 'ProjectHookManager'), ('keys', 'ProjectKeyManager'), ('issues', 'ProjectIssueManager'), ('labels', 'ProjectLabelManager'), ('members', 'ProjectMemberManager'), ('mergerequests', 'ProjectMergeRequestManager'), ('milestones', 'ProjectMilestoneManager'), ('notes', 'ProjectNoteManager'), ('notificationsettings', 'ProjectNotificationSettingsManager'), ('pagesdomains', 'ProjectPagesDomainManager'), ('pipelines', 'ProjectPipelineManager'), ('protectedbranches', 'ProjectProtectedBranchManager'), ('pipelineschedules', 'ProjectPipelineScheduleManager'), ('runners', 'ProjectRunnerManager'), ('services', 'ProjectServiceManager'), ('snippets', 'ProjectSnippetManager'), ('tags', 'ProjectTagManager'), ('users', 'ProjectUserManager'), ('triggers', 'ProjectTriggerManager'), ('variables', 'ProjectVariableManager'), ('wikis', 'ProjectWikiManager'), ) @cli.register_custom_action('Project', tuple(), ('path', 'ref')) @exc.on_http_error(exc.GitlabGetError) def repository_tree(self, path='', ref='', **kwargs): """Return a list of files in the repository. 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: GitlabAuthenticationError: If authentication is not correct GitlabGetError: If the server failed to perform the request Returns: list: The representation of the tree """ gl_path = '/projects/%s/repository/tree' % self.get_id() query_data = {} if path: query_data['path'] = path if ref: query_data['ref'] = ref return self.manager.gitlab.http_list(gl_path, query_data=query_data, **kwargs) @cli.register_custom_action('Project', ('sha', )) @exc.on_http_error(exc.GitlabGetError) def repository_blob(self, sha, **kwargs): """Return a file by blob SHA. Args: sha(str): ID of the blob **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabGetError: If the server failed to perform the request Returns: dict: The blob content and metadata """ path = '/projects/%s/repository/blobs/%s' % (self.get_id(), sha) return self.manager.gitlab.http_get(path, **kwargs) @cli.register_custom_action('Project', ('sha', )) @exc.on_http_error(exc.GitlabGetError) def repository_raw_blob(self, sha, streamed=False, action=None, chunk_size=1024, **kwargs): """Return the raw file contents for a blob. Args: sha(str): ID of the blob streamed (bool): If True the data will be processed by chunks of `chunk_size` and each chunk is passed to `action` for treatment action (callable): Callable responsible of dealing with chunk of data chunk_size (int): Size of each chunk **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabGetError: If the server failed to perform the request Returns: str: The blob content if streamed is False, None otherwise """ path = '/projects/%s/repository/blobs/%s/raw' % (self.get_id(), sha) result = self.manager.gitlab.http_get(path, streamed=streamed, **kwargs) return utils.response_content(result, streamed, action, chunk_size) @cli.register_custom_action('Project', ('from_', 'to')) @exc.on_http_error(exc.GitlabGetError) def repository_compare(self, from_, to, **kwargs): """Return a diff between two branches/commits. Args: from_(str): Source branch/SHA to(str): Destination branch/SHA **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabGetError: If the server failed to perform the request Returns: str: The diff """ path = '/projects/%s/repository/compare' % self.get_id() query_data = {'from': from_, 'to': to} return self.manager.gitlab.http_get(path, query_data=query_data, **kwargs) @cli.register_custom_action('Project') @exc.on_http_error(exc.GitlabGetError) def repository_contributors(self, **kwargs): """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: GitlabAuthenticationError: If authentication is not correct GitlabGetError: If the server failed to perform the request Returns: list: The contributors """ path = '/projects/%s/repository/contributors' % self.get_id() return self.manager.gitlab.http_list(path, **kwargs) @cli.register_custom_action('Project', tuple(), ('sha', )) @exc.on_http_error(exc.GitlabListError) def repository_archive(self, sha=None, streamed=False, action=None, chunk_size=1024, **kwargs): """Return a tarball of the repository. Args: sha (str): ID of the commit (default branch by default) streamed (bool): If True the data will be processed by chunks of `chunk_size` and each chunk is passed to `action` for treatment action (callable): Callable responsible of dealing with chunk of data chunk_size (int): Size of each chunk **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabListError: If the server failed to perform the request Returns: str: The binary data of the archive """ path = '/projects/%s/repository/archive' % self.get_id() query_data = {} if sha: query_data['sha'] = sha result = self.manager.gitlab.http_get(path, query_data=query_data, streamed=streamed, **kwargs) return utils.response_content(result, streamed, action, chunk_size) @cli.register_custom_action('Project', ('forked_from_id', )) @exc.on_http_error(exc.GitlabCreateError) def create_fork_relation(self, forked_from_id, **kwargs): """Create a forked from/to relation between existing projects. Args: forked_from_id (int): The ID of the project that was forked from **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabCreateError: If the relation could not be created """ path = '/projects/%s/fork/%s' % (self.get_id(), forked_from_id) self.manager.gitlab.http_post(path, **kwargs) @cli.register_custom_action('Project') @exc.on_http_error(exc.GitlabDeleteError) def delete_fork_relation(self, **kwargs): """Delete a forked relation between existing projects. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabDeleteError: If the server failed to perform the request """ path = '/projects/%s/fork' % self.get_id() self.manager.gitlab.http_delete(path, **kwargs) @cli.register_custom_action('Project') @exc.on_http_error(exc.GitlabCreateError) def star(self, **kwargs): """Star a project. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabCreateError: If the server failed to perform the request """ path = '/projects/%s/star' % self.get_id() server_data = self.manager.gitlab.http_post(path, **kwargs) self._update_attrs(server_data) @cli.register_custom_action('Project') @exc.on_http_error(exc.GitlabDeleteError) def unstar(self, **kwargs): """Unstar a project. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabDeleteError: If the server failed to perform the request """ path = '/projects/%s/unstar' % self.get_id() server_data = self.manager.gitlab.http_post(path, **kwargs) self._update_attrs(server_data) @cli.register_custom_action('Project') @exc.on_http_error(exc.GitlabCreateError) def archive(self, **kwargs): """Archive a project. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabCreateError: If the server failed to perform the request """ path = '/projects/%s/archive' % self.get_id() server_data = self.manager.gitlab.http_post(path, **kwargs) self._update_attrs(server_data) @cli.register_custom_action('Project') @exc.on_http_error(exc.GitlabDeleteError) def unarchive(self, **kwargs): """Unarchive a project. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabDeleteError: If the server failed to perform the request """ path = '/projects/%s/unarchive' % self.get_id() server_data = self.manager.gitlab.http_post(path, **kwargs) self._update_attrs(server_data) @cli.register_custom_action('Project', ('group_id', 'group_access'), ('expires_at', )) @exc.on_http_error(exc.GitlabCreateError) def share(self, group_id, group_access, expires_at=None, **kwargs): """Share the project with a group. Args: group_id (int): ID of the group. group_access (int): Access level for the group. **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabCreateError: If the server failed to perform the request """ path = '/projects/%s/share' % self.get_id() data = {'group_id': group_id, 'group_access': group_access, 'expires_at': expires_at} self.manager.gitlab.http_post(path, post_data=data, **kwargs) # variables not supported in CLI @cli.register_custom_action('Project', ('ref', 'token')) @exc.on_http_error(exc.GitlabCreateError) def trigger_pipeline(self, ref, token, variables={}, **kwargs): """Trigger a CI build. See https://gitlab.com/help/ci/triggers/README.md#trigger-a-build Args: ref (str): Commit to build; can be a branch name or a tag token (str): The trigger token variables (dict): Variables passed to the build script **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabCreateError: If the server failed to perform the request """ path = '/projects/%s/trigger/pipeline' % self.get_id() post_data = {'ref': ref, 'token': token, 'variables': variables} attrs = self.manager.gitlab.http_post( path, post_data=post_data, **kwargs) return ProjectPipeline(self.pipelines, attrs) @cli.register_custom_action('Project') @exc.on_http_error(exc.GitlabHousekeepingError) def housekeeping(self, **kwargs): """Start the housekeeping task. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabHousekeepingError: If the server failed to perform the request """ path = '/projects/%s/housekeeping' % self.get_id() self.manager.gitlab.http_post(path, **kwargs) # see #56 - add file attachment features @cli.register_custom_action('Project', ('filename', 'filepath')) @exc.on_http_error(exc.GitlabUploadError) def upload(self, filename, filedata=None, filepath=None, **kwargs): """Upload the specified file into the project. .. note:: Either ``filedata`` or ``filepath`` *MUST* be specified. Args: filename (str): The name of the file being uploaded filedata (bytes): The raw data of the file being uploaded filepath (str): The path to a local file to upload (optional) Raises: GitlabConnectionError: If the server cannot be reached GitlabUploadError: If the file upload fails GitlabUploadError: If ``filedata`` and ``filepath`` are not specified GitlabUploadError: If both ``filedata`` and ``filepath`` are specified Returns: dict: A ``dict`` with the keys: * ``alt`` - The alternate text for the upload * ``url`` - The direct url to the uploaded file * ``markdown`` - Markdown for the uploaded file """ if filepath is None and filedata is None: raise GitlabUploadError("No file contents or path specified") if filedata is not None and filepath is not None: raise GitlabUploadError("File contents and file path specified") if filepath is not None: with open(filepath, "rb") as f: filedata = f.read() url = ('/projects/%(id)s/uploads' % { 'id': self.id, }) file_info = { 'file': (filename, filedata), } data = self.manager.gitlab.http_post(url, files=file_info) return { "alt": data['alt'], "url": data['url'], "markdown": data['markdown'] } class ProjectManager(CRUDMixin, RESTManager): _path = '/projects' _obj_cls = Project _create_attrs = ( ('name', ), ('path', 'namespace_id', 'description', 'issues_enabled', 'merge_requests_enabled', 'jobs_enabled', 'wiki_enabled', 'snippets_enabled', 'container_registry_enabled', 'shared_runners_enabled', 'visibility', 'import_url', 'public_jobs', 'only_allow_merge_if_build_succeeds', 'only_allow_merge_if_all_discussions_are_resolved', 'lfs_enabled', 'request_access_enabled', 'printing_merge_request_link_enabled') ) _update_attrs = ( tuple(), ('name', 'path', 'default_branch', 'description', 'issues_enabled', 'merge_requests_enabled', 'jobs_enabled', 'wiki_enabled', 'snippets_enabled', 'container_registry_enabled', 'shared_runners_enabled', 'visibility', 'import_url', 'public_jobs', 'only_allow_merge_if_build_succeeds', 'only_allow_merge_if_all_discussions_are_resolved', 'lfs_enabled', 'request_access_enabled', 'printing_merge_request_link_enabled') ) _list_filters = ('search', 'owned', 'starred', 'archived', 'visibility', 'order_by', 'sort', 'simple', 'membership', 'statistics', 'with_issues_enabled', 'with_merge_requests_enabled', 'custom_attributes') class Runner(SaveMixin, ObjectDeleteMixin, RESTObject): pass class RunnerManager(RetrieveMixin, UpdateMixin, DeleteMixin, RESTManager): _path = '/runners' _obj_cls = Runner _update_attrs = (tuple(), ('description', 'active', 'tag_list')) _list_filters = ('scope', ) @cli.register_custom_action('RunnerManager', tuple(), ('scope', )) @exc.on_http_error(exc.GitlabListError) def all(self, scope=None, **kwargs): """List all the runners. Args: scope (str): The scope of runners to show, one of: specific, shared, active, paused, online 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 server failed to perform the request Returns: list(Runner): a list of runners matching the scope. """ path = '/runners/all' query_data = {} if scope is not None: query_data['scope'] = scope return self.gitlab.http_list(path, query_data, **kwargs) class Todo(ObjectDeleteMixin, RESTObject): @cli.register_custom_action('Todo') @exc.on_http_error(exc.GitlabTodoError) def mark_as_done(self, **kwargs): """Mark the todo as done. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabTodoError: If the server failed to perform the request """ path = '%s/%s/mark_as_done' % (self.manager.path, self.id) server_data = self.manager.gitlab.http_post(path, **kwargs) self._update_attrs(server_data) class TodoManager(GetFromListMixin, DeleteMixin, RESTManager): _path = '/todos' _obj_cls = Todo _list_filters = ('action', 'author_id', 'project_id', 'state', 'type') @cli.register_custom_action('TodoManager') @exc.on_http_error(exc.GitlabTodoError) def mark_all_as_done(self, **kwargs): """Mark all the todos as done. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabTodoError: If the server failed to perform the request Returns: int: The number of todos maked done """ result = self.gitlab.http_post('/todos/mark_as_done', **kwargs) try: return int(result) except ValueError: return 0 python-gitlab-1.3.0/requirements.txt000066400000000000000000000000211324224150200175440ustar00rootroot00000000000000requests>1.0 six python-gitlab-1.3.0/rtd-requirements.txt000066400000000000000000000000471324224150200203430ustar00rootroot00000000000000-r requirements.txt jinja2 sphinx>=1.3 python-gitlab-1.3.0/setup.py000066400000000000000000000027671324224150200160150ustar00rootroot00000000000000#!/usr/bin/python # -*- coding: utf-8 -*- from setuptools import setup from setuptools import find_packages def get_version(): with open('gitlab/__init__.py') as f: for line in f: if line.startswith('__version__'): return eval(line.split('=')[-1]) setup(name='python-gitlab', version=get_version(), description='Interact with GitLab API', long_description='Interact with GitLab API', author='Gauvain Pocentek', author_email='gauvain@pocentek.net', license='LGPLv3', url='https://github.com/python-gitlab/python-gitlab', packages=find_packages(), install_requires=['requests>=1.0', 'six'], entry_points={ 'console_scripts': [ 'gitlab = gitlab.cli:main' ] }, classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Console', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)', 'Natural Language :: English', 'Operating System :: POSIX', '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', ] ) python-gitlab-1.3.0/test-requirements.txt000066400000000000000000000001471324224150200205320ustar00rootroot00000000000000coverage discover testrepository hacking>=0.9.2,<0.10 httmock jinja2 mock sphinx>=1.3 sphinx_rtd_theme python-gitlab-1.3.0/tools/000077500000000000000000000000001324224150200154275ustar00rootroot00000000000000python-gitlab-1.3.0/tools/build_test_env.sh000077500000000000000000000071771324224150200210100ustar00rootroot00000000000000#!/bin/sh # Copyright (C) 2016 Gauvain Pocentek # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . pecho() { printf %s\\n "$*"; } log() { [ "$#" -eq 0 ] || { pecho "$@"; return 0; } while IFS= read -r log_line || [ -n "${log_line}" ]; do log "${log_line}" done } error() { log "ERROR: $@" >&2; } fatal() { error "$@"; exit 1; } try() { "$@" || fatal "'$@' failed"; } NOVENV= PY_VER=2 API_VER=4 while getopts :np:a: opt "$@"; do case $opt in n) NOVENV=1;; p) PY_VER=$OPTARG;; a) API_VER=$OPTARG;; :) fatal "Option -${OPTARG} requires a value";; '?') fatal "Unknown option: -${OPTARG}";; *) fatal "Internal error: opt=${opt}";; esac done case $PY_VER in 2) VENV_CMD=virtualenv;; 3) VENV_CMD=pyvenv;; *) fatal "Wrong python version (2 or 3)";; esac case $API_VER in 3|4) ;; *) fatal "Wrong API version (3 or 4)";; esac for req in \ curl \ docker \ "${VENV_CMD}" \ ; do command -v "${req}" >/dev/null 2>&1 || fatal "${req} is required" done VENV=$(pwd)/.venv || exit 1 CONFIG=/tmp/python-gitlab.cfg cleanup() { rm -f "${CONFIG}" log "Deactivating Python virtualenv..." command -v deactivate >/dev/null 2>&1 && deactivate || true log "Deleting python virtualenv..." rm -rf "$VENV" log "Stopping gitlab-test docker container..." docker rm -f gitlab-test >/dev/null log "Done." } [ -z "${BUILD_TEST_ENV_AUTO_CLEANUP+set}" ] || { trap cleanup EXIT trap 'exit 1' HUP INT TERM } try docker run --name gitlab-test --detach --publish 8080:80 \ --publish 2222:22 gpocentek/test-python-gitlab:latest >/dev/null LOGIN='root' PASSWORD='5iveL!fe' GITLAB() { gitlab --config-file "$CONFIG" "$@"; } GREEN='\033[0;32m' NC='\033[0m' OK() { printf "${GREEN}OK${NC}\\n"; } testcase() { testname=$1; shift testscript=$1; shift printf %s "Testing ${testname}... " eval "${testscript}" || fatal "test failed" 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 sleep 1 docker top gitlab-test >/dev/null 2>&1 || fatal "docker failed to start" sleep 4 curl -s http://localhost:8080/users/sign_in 2>/dev/null \ | grep -q "GitLab Community Edition" && break I=$((I+5)) [ "$I" -lt 120 ] || fatal "timed out" done # Get the token TOKEN=$($(dirname $0)/generate_token.py) cat > $CONFIG << EOF [global] default = local timeout = 10 [local] url = http://localhost:8080 private_token = $TOKEN api_version = $API_VER EOF log "Config file content ($CONFIG):" log <$CONFIG log "Pausing to give GitLab some time to finish starting up..." sleep 30 log "Test environment initialized." python-gitlab-1.3.0/tools/cli_test_v3.sh000066400000000000000000000070231324224150200202030ustar00rootroot00000000000000#!/bin/sh # Copyright (C) 2015 Gauvain Pocentek # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . testcase "project creation" ' OUTPUT=$(try GITLAB project create --name test-project1) || exit 1 PROJECT_ID=$(pecho "${OUTPUT}" | grep ^id: | cut -d" " -f2) OUTPUT=$(try GITLAB project list) || exit 1 pecho "${OUTPUT}" | grep -q test-project1 ' testcase "project update" ' GITLAB project update --id "$PROJECT_ID" --description "My New Description" ' testcase "user creation" ' OUTPUT=$(GITLAB user create --email fake@email.com --username user1 \ --name "User One" --password fakepassword) ' USER_ID=$(pecho "${OUTPUT}" | grep ^id: | cut -d' ' -f2) testcase "user get (by id)" ' GITLAB user get --id $USER_ID >/dev/null 2>&1 ' testcase "user get (by username)" ' GITLAB user get-by-username --query user1 >/dev/null 2>&1 ' testcase "verbose output" ' OUTPUT=$(try GITLAB -v user list) || exit 1 pecho "${OUTPUT}" | grep -q avatar-url ' testcase "CLI args not in output" ' OUTPUT=$(try GITLAB -v user list) || exit 1 pecho "${OUTPUT}" | grep -qv config-file ' testcase "adding member to a project" ' GITLAB project-member create --project-id "$PROJECT_ID" \ --user-id "$USER_ID" --access-level 40 >/dev/null 2>&1 ' testcase "file creation" ' GITLAB project-file create --project-id "$PROJECT_ID" \ --file-path README --branch-name master --content "CONTENT" \ --commit-message "Initial commit" >/dev/null 2>&1 ' testcase "issue creation" ' OUTPUT=$(GITLAB project-issue create --project-id "$PROJECT_ID" \ --title "my issue" --description "my issue description") ' ISSUE_ID=$(pecho "${OUTPUT}" | grep ^id: | cut -d' ' -f2) testcase "note creation" ' GITLAB project-issue-note create --project-id "$PROJECT_ID" \ --issue-id "$ISSUE_ID" --body "the body" >/dev/null 2>&1 ' testcase "branch creation" ' GITLAB project-branch create --project-id "$PROJECT_ID" \ --branch-name branch1 --ref master >/dev/null 2>&1 ' GITLAB project-file create --project-id "$PROJECT_ID" \ --file-path README2 --branch-name branch1 --content "CONTENT" \ --commit-message "second commit" >/dev/null 2>&1 testcase "merge request creation" ' OUTPUT=$(GITLAB project-merge-request create \ --project-id "$PROJECT_ID" \ --source-branch branch1 --target-branch master \ --title "Update README") ' MR_ID=$(pecho "${OUTPUT}" | grep ^id: | cut -d' ' -f2) testcase "merge request validation" ' GITLAB project-merge-request merge --project-id "$PROJECT_ID" \ --id "$MR_ID" >/dev/null 2>&1 ' testcase "branch deletion" ' GITLAB project-branch delete --project-id "$PROJECT_ID" \ --name branch1 >/dev/null 2>&1 ' testcase "project upload" ' GITLAB project upload --id "$PROJECT_ID" --filename '$(basename $0)' --filepath '$0' ' testcase "project deletion" ' GITLAB project delete --id "$PROJECT_ID" ' python-gitlab-1.3.0/tools/cli_test_v4.sh000066400000000000000000000072041324224150200202050ustar00rootroot00000000000000#!/bin/sh # Copyright (C) 2015 Gauvain Pocentek # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . testcase "project creation" ' OUTPUT=$(try GITLAB project create --name test-project1) || exit 1 PROJECT_ID=$(pecho "${OUTPUT}" | grep ^id: | cut -d" " -f2) OUTPUT=$(try GITLAB project list) || exit 1 pecho "${OUTPUT}" | grep -q test-project1 ' testcase "project update" ' GITLAB project update --id "$PROJECT_ID" --description "My New Description" ' testcase "user creation" ' OUTPUT=$(GITLAB user create --email fake@email.com --username user1 \ --name "User One" --password fakepassword) ' USER_ID=$(pecho "${OUTPUT}" | grep ^id: | cut -d' ' -f2) testcase "user get (by id)" ' GITLAB user get --id $USER_ID >/dev/null 2>&1 ' testcase "verbose output" ' OUTPUT=$(try GITLAB -v user list) || exit 1 pecho "${OUTPUT}" | grep -q avatar-url ' testcase "CLI args not in output" ' OUTPUT=$(try GITLAB -v user list) || exit 1 pecho "${OUTPUT}" | grep -qv config-file ' testcase "adding member to a project" ' GITLAB project-member create --project-id "$PROJECT_ID" \ --user-id "$USER_ID" --access-level 40 >/dev/null 2>&1 ' testcase "file creation" ' GITLAB project-file create --project-id "$PROJECT_ID" \ --file-path README --branch master --content "CONTENT" \ --commit-message "Initial commit" >/dev/null 2>&1 ' testcase "issue creation" ' OUTPUT=$(GITLAB project-issue create --project-id "$PROJECT_ID" \ --title "my issue" --description "my issue description") ' ISSUE_ID=$(pecho "${OUTPUT}" | grep ^iid: | cut -d' ' -f2) testcase "note creation" ' GITLAB project-issue-note create --project-id "$PROJECT_ID" \ --issue-iid "$ISSUE_ID" --body "the body" >/dev/null 2>&1 ' testcase "branch creation" ' GITLAB project-branch create --project-id "$PROJECT_ID" \ --branch branch1 --ref master >/dev/null 2>&1 ' GITLAB project-file create --project-id "$PROJECT_ID" \ --file-path README2 --branch branch1 --content "CONTENT" \ --commit-message "second commit" >/dev/null 2>&1 testcase "merge request creation" ' OUTPUT=$(GITLAB project-merge-request create \ --project-id "$PROJECT_ID" \ --source-branch branch1 --target-branch master \ --title "Update README") ' MR_ID=$(pecho "${OUTPUT}" | grep ^iid: | cut -d' ' -f2) testcase "merge request validation" ' GITLAB project-merge-request merge --project-id "$PROJECT_ID" \ --iid "$MR_ID" >/dev/null 2>&1 ' testcase "branch deletion" ' GITLAB project-branch delete --project-id "$PROJECT_ID" \ --name branch1 >/dev/null 2>&1 ' testcase "project upload" ' GITLAB project upload --id "$PROJECT_ID" \ --filename '$(basename $0)' --filepath '$0' >/dev/null 2>&1 ' testcase "project deletion" ' GITLAB project delete --id "$PROJECT_ID" ' testcase "application settings get" ' GITLAB application-settings get >/dev/null 2>&1 ' testcase "application settings update" ' GITLAB application-settings update --signup-enabled false ' python-gitlab-1.3.0/tools/functional_tests.sh000077500000000000000000000016011324224150200213500ustar00rootroot00000000000000#!/bin/sh # Copyright (C) 2015 Gauvain Pocentek # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . setenv_script=$(dirname "$0")/build_test_env.sh || exit 1 BUILD_TEST_ENV_AUTO_CLEANUP=true . "$setenv_script" "$@" || exit 1 . $(dirname "$0")/cli_test_v${API_VER}.sh python-gitlab-1.3.0/tools/generate_token.py000077500000000000000000000032201324224150200207730ustar00rootroot00000000000000#!/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() python-gitlab-1.3.0/tools/py_functional_tests.sh000077500000000000000000000016171324224150200220670ustar00rootroot00000000000000#!/bin/sh # Copyright (C) 2015 Gauvain Pocentek # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . setenv_script=$(dirname "$0")/build_test_env.sh || exit 1 BUILD_TEST_ENV_AUTO_CLEANUP=true . "$setenv_script" "$@" || exit 1 try python "$(dirname "$0")"/python_test_v${API_VER}.py python-gitlab-1.3.0/tools/python_test_v3.py000066400000000000000000000272111324224150200207740ustar00rootroot00000000000000import base64 import re import time import gitlab LOGIN = 'root' PASSWORD = '5iveL!fe' SSH_KEY = ("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDZAjAX8vTiHD7Yi3/EzuVaDChtih" "79HyJZ6H9dEqxFfmGA1YnncE0xujQ64TCebhkYJKzmTJCImSVkOu9C4hZgsw6eE76n" "+Cg3VwEeDUFy+GXlEJWlHaEyc3HWioxgOALbUp3rOezNh+d8BDwwqvENGoePEBsz5l" "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") # token authentication from config file gl = gitlab.Gitlab.from_config(config_files=['/tmp/python-gitlab.cfg']) gl.auth() assert(isinstance(gl.user, gitlab.v3.objects.CurrentUser)) # settings settings = gl.settings.get() settings.default_projects_limit = 42 settings.save() settings = gl.settings.get() assert(settings.default_projects_limit == 42) # user manipulations new_user = gl.users.create({'email': 'foo@bar.com', 'username': 'foo', 'name': 'foo', 'password': 'foo_password'}) users_list = gl.users.list() for user in users_list: if user.username == 'foo': break assert(new_user.username == user.username) assert(new_user.email == user.email) new_user.block() new_user.unblock() foobar_user = gl.users.create( {'email': 'foobar@example.com', 'username': 'foobar', 'name': 'Foo Bar', 'password': 'foobar_password'}) assert(gl.users.search('foobar')[0].id == foobar_user.id) usercmp = lambda x,y: cmp(x.id, y.id) expected = sorted([new_user, foobar_user], cmp=usercmp) actual = sorted(gl.users.search('foo'), cmp=usercmp) assert len(expected) == len(actual) assert len(gl.users.search('asdf')) == 0 assert gl.users.get_by_username('foobar').id == foobar_user.id assert gl.users.get_by_username('foo').id == new_user.id try: gl.users.get_by_username('asdf') except gitlab.GitlabGetError: pass else: assert False # SSH keys key = new_user.keys.create({'title': 'testkey', 'key': SSH_KEY}) assert(len(new_user.keys.list()) == 1) key.delete() assert(len(new_user.keys.list()) == 0) # emails email = new_user.emails.create({'email': 'foo2@bar.com'}) assert(len(new_user.emails.list()) == 1) email.delete() assert(len(new_user.emails.list()) == 0) new_user.delete() foobar_user.delete() assert(len(gl.users.list()) == 3) # current user key key = gl.user.keys.create({'title': 'testkey', 'key': SSH_KEY}) assert(len(gl.user.keys.list()) == 1) key.delete() # groups user1 = gl.users.create({'email': 'user1@test.com', 'username': 'user1', 'name': 'user1', 'password': 'user1_pass'}) user2 = gl.users.create({'email': 'user2@test.com', 'username': 'user2', 'name': 'user2', 'password': 'user2_pass'}) group1 = gl.groups.create({'name': 'group1', 'path': 'group1'}) group2 = gl.groups.create({'name': 'group2', 'path': 'group2'}) 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("oup1")) == 1) assert(group3.parent_id == p_id) group1.members.create({'access_level': gitlab.Group.OWNER_ACCESS, 'user_id': user1.id}) group1.members.create({'access_level': gitlab.Group.GUEST_ACCESS, 'user_id': user2.id}) group2.members.create({'access_level': gitlab.Group.OWNER_ACCESS, 'user_id': user2.id}) # Administrator belongs to the groups assert(len(group1.members.list()) == 3) assert(len(group2.members.list()) == 2) group1.members.delete(user1.id) assert(len(group1.members.list()) == 2) member = group1.members.get(user2.id) member.access_level = gitlab.Group.OWNER_ACCESS member.save() member = group1.members.get(user2.id) assert(member.access_level == gitlab.Group.OWNER_ACCESS) group2.members.delete(gl.user.id) # hooks hook = gl.hooks.create({'url': 'http://whatever.com'}) assert(len(gl.hooks.list()) == 1) hook.delete() assert(len(gl.hooks.list()) == 0) # projects admin_project = gl.projects.create({'name': 'admin_project'}) gr1_project = gl.projects.create({'name': 'gr1_project', 'namespace_id': group1.id}) gr2_project = gl.projects.create({'name': 'gr2_project', 'namespace_id': group2.id}) sudo_project = gl.projects.create({'name': 'sudo_project'}, sudo=user1.name) assert(len(gl.projects.all()) == 4) assert(len(gl.projects.owned()) == 2) assert(len(gl.projects.list(search="admin")) == 1) # test pagination l1 = gl.projects.list(per_page=1, page=1) l2 = gl.projects.list(per_page=1, page=2) assert(len(l1) == 1) assert(len(l2) == 1) assert(l1[0].id != l2[0].id) # project content (files) admin_project.files.create({'file_path': 'README', 'branch_name': 'master', 'content': 'Initial content', 'commit_message': 'Initial commit'}) readme = admin_project.files.get(file_path='README', ref='master') readme.content = base64.b64encode("Improved README") time.sleep(2) readme.save(branch_name="master", commit_message="new commit") readme.delete(commit_message="Removing README", branch_name="master") admin_project.files.create({'file_path': 'README.rst', 'branch_name': 'master', 'content': 'Initial content', 'commit_message': 'New commit'}) 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) == 2) assert(tree[0]['name'] == 'README.rst') blob = admin_project.repository_blob('master', 'README.rst') assert(blob == 'Initial content') archive1 = admin_project.repository_archive() archive2 = admin_project.repository_archive('master') assert(archive1 == archive2) # project file uploads filename = "test.txt" file_contents = "testing contents" uploaded_file = admin_project.upload(filename, file_contents) assert(uploaded_file["alt"] == filename) assert(uploaded_file["url"].startswith("/uploads/")) assert(uploaded_file["url"].endswith("/" + filename)) assert(uploaded_file["markdown"] == "[{}]({})".format( uploaded_file["alt"], uploaded_file["url"], )) # 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') assert(len(admin_project.labels.list()) == 1) label1.new_name = 'label1updated' label1.save() assert(label1.name == 'label1updated') label1.subscribe() assert(label1.subscribed == True) label1.unsubscribe() assert(label1.subscribed == False) label1.delete() # milestones m1 = admin_project.milestones.create({'title': 'milestone1'}) assert(len(admin_project.milestones.list()) == 1) m1.due_date = '2020-01-01T00:00:00Z' m1.save() m1.state_event = 'close' m1.save() m1 = admin_project.milestones.get(1) assert(m1.state == 'closed') # issues issue1 = admin_project.issues.create({'title': 'my issue 1', 'milestone_id': m1.id}) issue2 = admin_project.issues.create({'title': 'my issue 2'}) issue3 = admin_project.issues.create({'title': 'my issue 3'}) assert(len(admin_project.issues.list()) == 3) issue3.state_event = 'close' issue3.save() assert(len(admin_project.issues.list(state='closed')) == 1) assert(len(admin_project.issues.list(state='opened')) == 2) assert(len(admin_project.issues.list(milestone='milestone1')) == 1) assert(m1.issues()[0].title == 'my issue 1') # tags tag1 = admin_project.tags.create({'tag_name': 'v1.0', 'ref': 'master'}) assert(len(admin_project.tags.list()) == 1) tag1.set_release_description('Description 1') tag1.set_release_description('Description 2') assert(tag1.release.description == 'Description 2') tag1.delete() # triggers tr1 = admin_project.triggers.create({}) assert(len(admin_project.triggers.list()) == 1) tr1 = admin_project.triggers.get(tr1.token) tr1.delete() # variables v1 = admin_project.variables.create({'key': 'key1', 'value': 'value1'}) assert(len(admin_project.variables.list()) == 1) v1.value = 'new_value1' v1.save() v1 = admin_project.variables.get(v1.key) assert(v1.value == 'new_value1') v1.delete() # branches and merges to_merge = admin_project.branches.create({'branch_name': 'branch1', 'ref': 'master'}) admin_project.files.create({'file_path': 'README2.rst', 'branch_name': 'branch1', 'content': 'Initial content', 'commit_message': 'New commit in new branch'}) mr = admin_project.mergerequests.create({'source_branch': 'branch1', 'target_branch': 'master', 'title': 'MR readme2'}) ret = mr.merge() admin_project.branches.delete('branch1') try: mr.merge() except gitlab.GitlabMRClosedError: pass # stars admin_project = admin_project.star() assert(admin_project.star_count == 1) admin_project = admin_project.unstar() assert(admin_project.star_count == 0) # project boards #boards = admin_project.boards.list() #assert(len(boards)) #board = boards[0] #lists = board.lists.list() #begin_size = len(lists) #last_list = lists[-1] #last_list.position = 0 #last_list.save() #last_list.delete() #lists = board.lists.list() #assert(len(lists) == begin_size - 1) # namespaces ns = gl.namespaces.list() assert(len(ns) != 0) ns = gl.namespaces.list(search='root')[0] assert(ns.kind == 'user') # broadcast messages msg = gl.broadcastmessages.create({'message': 'this is the message'}) msg.color = '#444444' msg.save() msg = gl.broadcastmessages.list()[0] assert(msg.color == '#444444') msg = gl.broadcastmessages.get(1) assert(msg.color == '#444444') msg.delete() assert(len(gl.broadcastmessages.list()) == 0) # notification settings settings = gl.notificationsettings.get() settings.level = gitlab.NOTIFICATION_LEVEL_WATCH settings.save() 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) snippet = gl.snippets.create({'title': 'snippet1', 'file_name': 'snippet1.py', 'content': 'import gitlab'}) snippet = gl.snippets.get(1) snippet.title = 'updated_title' snippet.save() snippet = gl.snippets.get(1) assert(snippet.title == 'updated_title') content = snippet.raw() assert(content == 'import gitlab') snippet.delete() assert(len(gl.snippets.list()) == 0) python-gitlab-1.3.0/tools/python_test_v4.py000066400000000000000000000527561324224150200210110ustar00rootroot00000000000000import base64 import time import gitlab LOGIN = 'root' PASSWORD = '5iveL!fe' SSH_KEY = ("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDZAjAX8vTiHD7Yi3/EzuVaDChtih" "79HyJZ6H9dEqxFfmGA1YnncE0xujQ64TCebhkYJKzmTJCImSVkOu9C4hZgsw6eE76n" "+Cg3VwEeDUFy+GXlEJWlHaEyc3HWioxgOALbUp3rOezNh+d8BDwwqvENGoePEBsz5l" "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") GPG_KEY = '''-----BEGIN PGP PUBLIC KEY BLOCK----- mQENBFn5mzYBCADH6SDVPAp1zh/hxmTi0QplkOfExBACpuY6OhzNdIg+8/528b3g Y5YFR6T/HLv/PmeHskUj21end1C0PNG2T9dTx+2Vlh9ISsSG1kyF9T5fvMR3bE0x Dl6S489CXZrjPTS9SHk1kF+7dwjUxLJyxF9hPiSihFefDFu3NeOtG/u8vbC1mewQ ZyAYue+mqtqcCIFFoBz7wHKMWjIVSJSyTkXExu4OzpVvy3l2EikbvavI3qNz84b+ Mgkv/kiBlNoCy3CVuPk99RYKZ3lX1vVtqQ0OgNGQvb4DjcpyjmbKyibuZwhDjIOh au6d1OyEbayTntd+dQ4j9EMSnEvm/0MJ4eXPABEBAAG0G0dpdGxhYlRlc3QxIDxm YWtlQGZha2UudGxkPokBNwQTAQgAIQUCWfmbNgIbAwULCQgHAgYVCAkKCwIEFgID AQIeAQIXgAAKCRBgxELHf8f3hF3yB/wNJlWPKY65UsB4Lo0hs1OxdxCDqXogSi0u 6crDEIiyOte62pNZKzWy8TJcGZvznRTZ7t8hXgKFLz3PRMcl+vAiRC6quIDUj+2V eYfwaItd1lUfzvdCaC7Venf4TQ74f5vvNg/zoGwE6eRoSbjlLv9nqsxeA0rUBUQL LYikWhVMP3TrlfgfduYvh6mfgh57BDLJ9kJVpyfxxx9YLKZbaas9sPa6LgBtR555 JziUxHmbEv8XCsUU8uoFeP1pImbNBplqE3wzJwzOMSmmch7iZzrAwfN7N2j3Wj0H B5kQddJ9dmB4BbU0IXGhWczvdpxboI2wdY8a1JypxOdePoph/43iuQENBFn5mzYB CADnTPY0Zf3d9zLjBNgIb3yDl94uOcKCq0twNmyjMhHzGqw+UMe9BScy34GL94Al xFRQoaL+7P8hGsnsNku29A/VDZivcI+uxTx4WQ7OLcn7V0bnHV4d76iky2ufbUt/ GofthjDs1SonePO2N09sS4V4uK0d5N4BfCzzXgvg8etCLxNmC9BGt7AaKUUzKBO4 2QvNNaC2C/8XEnOgNWYvR36ylAXAmo0sGFXUsBCTiq1fugS9pwtaS2JmaVpZZ3YT pMZlS0+SjC5BZYFqSmKCsA58oBRzCxQz57nR4h5VEflgD+Hy0HdW0UHETwz83E6/ U0LL6YyvhwFr6KPq5GxinSvfABEBAAGJAR8EGAEIAAkFAln5mzYCGwwACgkQYMRC x3/H94SJgwgAlKQb10/xcL/epdDkR7vbiei7huGLBpRDb/L5fM8B5W77Qi8Xmuqj cCu1j99ZCA5hs/vwVn8j8iLSBGMC5gxcuaar/wtmiaEvT9fO/h6q4opG7NcuiJ8H wRj8ccJmRssNqDD913PLz7T40Ts62blhrEAlJozGVG/q7T3RAZcskOUHKeHfc2RI YzGsC/I9d7k6uxAv1L9Nm5F2HaAQDzhkdd16nKkGaPGR35cT1JLInkfl5cdm7ldN nxs4TLO3kZjUTgWKdhpgRNF5hwaz51ZjpebaRf/ZqRuNyX4lIRolDxzOn/+O1o8L qG2ZdhHHmSK2LaQLFiSprUkikStNU9BqSQ== =5OGa -----END PGP PUBLIC KEY BLOCK-----''' # token authentication from config file gl = gitlab.Gitlab.from_config(config_files=['/tmp/python-gitlab.cfg']) gl.auth() assert(isinstance(gl.user, gitlab.v4.objects.CurrentUser)) # sidekiq out = gl.sidekiq.queue_metrics() assert(isinstance(out, dict)) assert('pages' in out['queues']) out = gl.sidekiq.process_metrics() assert(isinstance(out, dict)) assert('hostname' in out['processes'][0]) out = gl.sidekiq.job_stats() assert(isinstance(out, dict)) assert('processed' in out['jobs']) out = gl.sidekiq.compound_metrics() assert(isinstance(out, dict)) assert('jobs' in out) assert('processes' in out) assert('queues' in out) # settings settings = gl.settings.get() settings.default_projects_limit = 42 settings.save() settings = gl.settings.get() assert(settings.default_projects_limit == 42) # users new_user = gl.users.create({'email': 'foo@bar.com', 'username': 'foo', 'name': 'foo', 'password': 'foo_password'}) users_list = gl.users.list() for user in users_list: if user.username == 'foo': break assert(new_user.username == user.username) 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'}) assert gl.users.list(search='foobar')[0].id == foobar_user.id usercmp = lambda x,y: cmp(x.id, y.id) expected = sorted([new_user, foobar_user], cmp=usercmp) actual = sorted(list(gl.users.list(search='foo')), cmp=usercmp) assert len(expected) == len(actual) assert len(gl.users.list(search='asdf')) == 0 foobar_user.bio = 'This is the user bio' foobar_user.save() # GPG keys gkey = new_user.gpgkeys.create({'key': GPG_KEY}) assert(len(new_user.gpgkeys.list()) == 1) # Seems broken on the gitlab side # gkey = new_user.gpgkeys.get(gkey.id) gkey.delete() assert(len(new_user.gpgkeys.list()) == 0) # SSH keys key = new_user.keys.create({'title': 'testkey', 'key': SSH_KEY}) assert(len(new_user.keys.list()) == 1) key.delete() assert(len(new_user.keys.list()) == 0) # emails email = new_user.emails.create({'email': 'foo2@bar.com'}) assert(len(new_user.emails.list()) == 1) email.delete() assert(len(new_user.emails.list()) == 0) # custom attributes 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) attr = new_user.customattributes.set('key', 'value2') attr = new_user.customattributes.get('key') assert(attr.value == 'value2') assert(len(new_user.customattributes.list()) == 1) attr.delete() assert(len(new_user.customattributes.list()) == 0) # impersonation tokens user_token = new_user.impersonationtokens.create( {'name': 'token1', 'scopes': ['api', 'read_user']}) l = new_user.impersonationtokens.list(state='active') assert(len(l) == 1) user_token.delete() l = new_user.impersonationtokens.list(state='active') assert(len(l) == 0) l = new_user.impersonationtokens.list(state='inactive') assert(len(l) == 1) new_user.delete() foobar_user.delete() assert(len(gl.users.list()) == 3) # current user mail mail = gl.user.emails.create({'email': 'current@user.com'}) assert(len(gl.user.emails.list()) == 1) mail.delete() assert(len(gl.user.emails.list()) == 0) # current user GPG keys gkey = gl.user.gpgkeys.create({'key': GPG_KEY}) assert(len(gl.user.gpgkeys.list()) == 1) # Seems broken on the gitlab side gkey = gl.user.gpgkeys.get(gkey.id) gkey.delete() assert(len(gl.user.gpgkeys.list()) == 0) # current user key key = gl.user.keys.create({'title': 'testkey', 'key': SSH_KEY}) assert(len(gl.user.keys.list()) == 1) key.delete() assert(len(gl.user.keys.list()) == 0) # templates assert(gl.dockerfiles.list()) dockerfile = gl.dockerfiles.get('Node') assert(dockerfile.content is not None) assert(gl.gitignores.list()) gitignore = gl.gitignores.get('Node') assert(gitignore.content is not None) assert(gl.gitlabciymls.list()) gitlabciyml = gl.gitlabciymls.get('Nodejs') assert(gitlabciyml.content is not None) assert(gl.licenses.list()) license = gl.licenses.get('bsd-2-clause', project='mytestproject', fullname='mytestfullname') assert('mytestfullname' in license.content) # groups user1 = gl.users.create({'email': 'user1@test.com', 'username': 'user1', 'name': 'user1', 'password': 'user1_pass'}) user2 = gl.users.create({'email': 'user2@test.com', 'username': 'user2', 'name': 'user2', 'password': 'user2_pass'}) group1 = gl.groups.create({'name': 'group1', 'path': 'group1'}) group2 = gl.groups.create({'name': 'group2', 'path': 'group2'}) 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='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}) group1.members.create({'access_level': gitlab.Group.GUEST_ACCESS, 'user_id': user2.id}) group2.members.create({'access_level': gitlab.Group.OWNER_ACCESS, 'user_id': user2.id}) # Administrator belongs to the groups assert(len(group1.members.list()) == 3) assert(len(group2.members.list()) == 2) group1.members.delete(user1.id) assert(len(group1.members.list()) == 2) member = group1.members.get(user2.id) member.access_level = gitlab.Group.OWNER_ACCESS member.save() member = group1.members.get(user2.id) 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' settings.save() settings = group2.notificationsettings.get() assert(settings.level == 'disabled') # group milestones gm1 = group1.milestones.create({'title': 'groupmilestone1'}) assert(len(group1.milestones.list()) == 1) gm1.due_date = '2020-01-01T00:00:00Z' gm1.save() gm1.state_event = 'close' gm1.save() gm1 = group1.milestones.get(gm1.id) assert(gm1.state == 'closed') assert(len(gm1.issues()) == 0) assert(len(gm1.merge_requests()) == 0) # group variables group1.variables.create({'key': 'foo', 'value': 'bar'}) g_v = group1.variables.get('foo') assert(g_v.value == 'bar') g_v.value = 'baz' g_v.save() g_v = group1.variables.get('foo') assert(g_v.value == 'baz') assert(len(group1.variables.list()) == 1) g_v.delete() assert(len(group1.variables.list()) == 0) # hooks hook = gl.hooks.create({'url': 'http://whatever.com'}) assert(len(gl.hooks.list()) == 1) hook.delete() assert(len(gl.hooks.list()) == 0) # projects admin_project = gl.projects.create({'name': 'admin_project'}) gr1_project = gl.projects.create({'name': 'gr1_project', 'namespace_id': group1.id}) gr2_project = gl.projects.create({'name': 'gr2_project', 'namespace_id': group2.id}) sudo_project = gl.projects.create({'name': 'sudo_project'}, sudo=user1.name) assert(len(gl.projects.list(owned=True)) == 2) assert(len(gl.projects.list(search="admin")) == 1) # test pagination l1 = gl.projects.list(per_page=1, page=1) l2 = gl.projects.list(per_page=1, page=2) 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', 'content': 'Initial content', 'commit_message': 'Initial commit'}) readme = admin_project.files.get(file_path='README', ref='master') readme.content = base64.b64encode("Improved README") time.sleep(2) readme.save(branch="master", commit_message="new commit") readme.delete(commit_message="Removing README", branch="master") admin_project.files.create({'file_path': 'README.rst', 'branch': 'master', 'content': 'Initial content', 'commit_message': 'New commit'}) readme = admin_project.files.get(file_path='README.rst', ref='master') assert(readme.decode() == 'Initial content') data = { 'branch': 'master', 'commit_message': 'blah blah blah', 'actions': [ { 'action': 'create', 'file_path': 'blah', 'content': 'blah' } ] } admin_project.commits.create(data) assert('---' in admin_project.commits.list()[0].diff()[0]['diff']) # commit status commit = admin_project.commits.list()[0] status = commit.statuses.create({'state': 'success', 'sha': commit.id}) assert(len(commit.statuses.list()) == 1) # commit comment commit.comments.create({'note': 'This is a commit comment'}) assert(len(commit.comments.list()) == 1) # housekeeping admin_project.housekeeping() # repository tree = admin_project.repository_tree() assert(len(tree) != 0) assert(tree[0]['name'] == 'README.rst') blob_id = tree[0]['id'] blob = admin_project.repository_raw_blob(blob_id) assert(blob == 'Initial content') archive1 = admin_project.repository_archive() archive2 = admin_project.repository_archive('master') assert(archive1 == archive2) # project file uploads filename = "test.txt" file_contents = "testing contents" uploaded_file = admin_project.upload(filename, file_contents) assert(uploaded_file["alt"] == filename) assert(uploaded_file["url"].startswith("/uploads/")) assert(uploaded_file["url"].endswith("/" + filename)) assert(uploaded_file["markdown"] == "[{}]({})".format( uploaded_file["alt"], uploaded_file["url"], )) # environments admin_project.environments.create({'name': 'env1', 'external_url': 'http://fake.env/whatever'}) envs = admin_project.environments.list() assert(len(envs) == 1) env = admin_project.environments.get(envs[0].id) env.external_url = 'http://new.env/whatever' env.save() env = admin_project.environments.get(envs[0].id) assert(env.external_url == 'http://new.env/whatever') env.delete() assert(len(admin_project.environments.list()) == 0) # project events admin_project.events.list() # forks fork = admin_project.forks.create({'namespace': user1.username}) p = gl.projects.get(fork.id) assert(p.forked_from_project['id'] == admin_project.id) # project hooks hook = admin_project.hooks.create({'url': 'http://hook.url'}) assert(len(admin_project.hooks.list()) == 1) hook.note_events = True hook.save() hook = admin_project.hooks.get(hook.id) assert(hook.note_events is True) hook.delete() # deploy keys deploy_key = admin_project.keys.create({'title': 'foo@bar', 'key': DEPLOY_KEY}) project_keys = list(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.delete(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') assert(len(admin_project.labels.list()) == 1) label1.new_name = 'label1updated' label1.save() assert(label1.name == 'label1updated') label1.subscribe() assert(label1.subscribed == True) label1.unsubscribe() assert(label1.subscribed == False) label1.delete() # milestones m1 = admin_project.milestones.create({'title': 'milestone1'}) assert(len(admin_project.milestones.list()) == 1) m1.due_date = '2020-01-01T00:00:00Z' m1.save() m1.state_event = 'close' m1.save() m1 = admin_project.milestones.get(m1.id) assert(m1.state == 'closed') assert(len(m1.issues()) == 0) assert(len(m1.merge_requests()) == 0) # issues issue1 = admin_project.issues.create({'title': 'my issue 1', 'milestone_id': m1.id}) issue2 = admin_project.issues.create({'title': 'my issue 2'}) issue3 = admin_project.issues.create({'title': 'my issue 3'}) assert(len(admin_project.issues.list()) == 3) issue3.state_event = 'close' issue3.save() assert(len(admin_project.issues.list(state='closed')) == 1) assert(len(admin_project.issues.list(state='opened')) == 2) 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)) # tags tag1 = admin_project.tags.create({'tag_name': 'v1.0', 'ref': 'master'}) assert(len(admin_project.tags.list()) == 1) tag1.set_release_description('Description 1') tag1.set_release_description('Description 2') assert(tag1.release['description'] == 'Description 2') tag1.delete() # project snippet admin_project.snippets_enabled = True admin_project.save() snippet = admin_project.snippets.create( {'title': 'snip1', 'file_name': 'foo.py', 'code': 'initial content', 'visibility': gitlab.v4.objects.VISIBILITY_PRIVATE} ) snippet.file_name = 'bar.py' snippet.save() snippet = admin_project.snippets.get(snippet.id) assert(snippet.content() == 'initial content') assert(snippet.file_name == 'bar.py') size = len(admin_project.snippets.list()) snippet.delete() assert(len(admin_project.snippets.list()) == (size - 1)) # triggers tr1 = admin_project.triggers.create({'description': 'trigger1'}) assert(len(admin_project.triggers.list()) == 1) tr1.delete() # variables v1 = admin_project.variables.create({'key': 'key1', 'value': 'value1'}) assert(len(admin_project.variables.list()) == 1) v1.value = 'new_value1' v1.save() v1 = admin_project.variables.get(v1.key) assert(v1.value == 'new_value1') v1.delete() # branches and merges to_merge = admin_project.branches.create({'branch': 'branch1', 'ref': 'master'}) admin_project.files.create({'file_path': 'README2.rst', 'branch': 'branch1', 'content': 'Initial content', 'commit_message': 'New commit in new branch'}) 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') try: mr.merge() except gitlab.GitlabMRClosedError: pass # protected branches p_b = admin_project.protectedbranches.create({'name': '*-stable'}) assert(p_b.name == '*-stable') p_b = admin_project.protectedbranches.get('*-stable') # master is protected by default when a branch has been created assert(len(admin_project.protectedbranches.list()) == 2) admin_project.protectedbranches.delete('master') p_b.delete() assert(len(admin_project.protectedbranches.list()) == 0) # stars admin_project.star() assert(admin_project.star_count == 1) admin_project.unstar() assert(admin_project.star_count == 0) # project boards #boards = admin_project.boards.list() #assert(len(boards)) #board = boards[0] #lists = board.lists.list() #begin_size = len(lists) #last_list = lists[-1] #last_list.position = 0 #last_list.save() #last_list.delete() #lists = board.lists.list() #assert(len(lists) == begin_size - 1) # project wiki wiki_content = 'Wiki page content' wp = admin_project.wikis.create({'title': 'wikipage', 'content': wiki_content}) assert(len(admin_project.wikis.list()) == 1) wp = admin_project.wikis.get(wp.slug) assert(wp.content == wiki_content) # update and delete seem broken # wp.content = 'new content' # wp.save() # wp.delete() # assert(len(admin_project.wikis.list()) == 0) # namespaces ns = gl.namespaces.list(all=True) 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' msg.save() msg = gl.broadcastmessages.list(all=True)[0] assert(msg.color == '#444444') msg = gl.broadcastmessages.get(1) assert(msg.color == '#444444') msg.delete() assert(len(gl.broadcastmessages.list()) == 0) # notification settings settings = gl.notificationsettings.get() settings.level = gitlab.NOTIFICATION_LEVEL_WATCH settings.save() settings = gl.notificationsettings.get() assert(settings.level == gitlab.NOTIFICATION_LEVEL_WATCH) # services service = admin_project.services.get('asana') service.api_key = 'whatever' service.save() service = admin_project.services.get('asana') assert(service.active == True) service.delete() service = admin_project.services.get('asana') assert(service.active == False) # snippets snippets = gl.snippets.list(all=True) assert(len(snippets) == 0) snippet = gl.snippets.create({'title': 'snippet1', 'file_name': 'snippet1.py', 'content': 'import gitlab'}) snippet = gl.snippets.get(snippet.id) snippet.title = 'updated_title' snippet.save() snippet = gl.snippets.get(snippet.id) assert(snippet.title == 'updated_title') content = snippet.content() assert(content == 'import gitlab') snippet.delete() # user activities gl.user_activities.list() # events gl.events.list() python-gitlab-1.3.0/tox.ini000066400000000000000000000020331324224150200156000ustar00rootroot00000000000000[tox] minversion = 1.6 skipsdist = True envlist = py36,py35,py34,py27,pep8 [testenv] setenv = VIRTUAL_ENV={envdir} whitelist_externals = true usedevelop = True install_command = pip install {opts} {packages} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = python setup.py testr --testr-args='{posargs}' [testenv:pep8] commands = flake8 {posargs} gitlab/ [testenv:venv] commands = {posargs} [flake8] exclude = .git,.venv,.tox,dist,doc,*egg,build, ignore = H501,H803 [testenv:docs] commands = python setup.py build_sphinx [testenv:cover] commands = python setup.py testr --slowest --coverage --testr-args="{posargs}" coverage report --omit=*tests* coverage html --omit=*tests* [testenv:cli_func_v3] commands = {toxinidir}/tools/functional_tests.sh -a 3 [testenv:cli_func_v4] commands = {toxinidir}/tools/functional_tests.sh -a 4 [testenv:py_func_v3] commands = {toxinidir}/tools/py_functional_tests.sh -a 3 [testenv:py_func_v4] commands = {toxinidir}/tools/py_functional_tests.sh -a 4