pax_global_header00006660000000000000000000000064136165170100014512gustar00rootroot0000000000000052 comment=272db2655d80fb81fbe1d8c56f241fe9f31b47e0 python-gitlab-2.0.1/000077500000000000000000000000001361651701000142735ustar00rootroot00000000000000python-gitlab-2.0.1/.dockerignore000066400000000000000000000000471361651701000167500ustar00rootroot00000000000000venv/ dist/ build/ *.egg-info .github/ python-gitlab-2.0.1/.github/000077500000000000000000000000001361651701000156335ustar00rootroot00000000000000python-gitlab-2.0.1/.github/ISSUE_TEMPLATE.md000066400000000000000000000003501361651701000203360ustar00rootroot00000000000000## Description of the problem, including code/CLI snippet ## Expected Behavior ## Actual Behavior ## Specifications - python-gitlab version: - API version you are using (v3/v4): - Gitlab server version (or gitlab.com): python-gitlab-2.0.1/.gitignore000066400000000000000000000001351361651701000162620ustar00rootroot00000000000000*.pyc build/ dist/ MANIFEST .*.swp *.egg-info .idea/ docs/_build .testrepository/ .tox venv/ python-gitlab-2.0.1/.gitlab-ci.yml000066400000000000000000000023421361651701000167300ustar00rootroot00000000000000image: python:3.7 stages: - deploy - deploy-latest deploy: stage: deploy script: - pip install -U setuptools wheel twine - python setup.py sdist bdist_wheel # test package - python3 -m venv test - . test/bin/activate - pip install -U dist/python_gitlab*.whl - gitlab -h - deactivate - twine upload --skip-existing -u $TWINE_USERNAME -p $TWINE_PASSWORD dist/* only: - tags deploy_image: stage: deploy image: name: gcr.io/kaniko-project/executor:debug entrypoint: [""] script: - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG only: - tags deploy-latest: stage: deploy-latest image: name: gcr.io/go-containerregistry/crane:debug entrypoint: [""] script: - mkdir /root/.docker && echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /root/.docker/config.json - /ko-app/crane cp $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG $CI_REGISTRY_IMAGE:latest only: - tags python-gitlab-2.0.1/.testr.conf000066400000000000000000000002441361651701000163610ustar00rootroot00000000000000[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-2.0.1/.travis.yml000066400000000000000000000025611361651701000164100ustar00rootroot00000000000000sudo: required services: - docker language: python git: depth: false stages: - lint - test jobs: include: - stage: lint name: commitlint script: - npm install -g @commitlint/cli @commitlint/config-conventional - 'echo "module.exports = {extends: [\"@commitlint/config-conventional\"]}" > commitlint.config.js' - npx commitlint --from=origin/master - stage: lint name: black_lint dist: bionic python: 3.8 script: - pip3 install -U --pre black - black --check . - stage: test name: cli_func_v4 dist: bionic python: 3.8 script: - pip3 install tox - tox -e cli_func_v4 - stage: test name: py_func_v4 dist: bionic python: 3.8 script: - pip3 install tox - tox -e py_func_v4 - stage: test name: docs dist: bionic python: 3.8 script: - pip3 install tox - tox -e docs - stage: test name: py36 python: 3.6 dist: bionic script: - pip3 install tox - tox -e py36 - stage: test name: py37 dist: bionic python: 3.7 script: - pip3 install tox - tox -e py37 - stage: test dist: bionic name: py38 python: 3.8 script: - pip3 install tox - tox -e py38 python-gitlab-2.0.1/AUTHORS000066400000000000000000000003131361651701000153400ustar00rootroot00000000000000Authors / Maintainers --------------------- Gauvain Pocentek Max Wittig Contributors ------------ See ``git log`` for a full list of contributors. python-gitlab-2.0.1/COPYING000066400000000000000000000167431361651701000153410ustar00rootroot00000000000000 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-2.0.1/ChangeLog.rst000066400000000000000000000700711361651701000166610ustar00rootroot00000000000000ChangeLog - Moved to GitHub releases ==================================== The changes of newer versions can be found at https://github.com/python-gitlab/python-gitlab/releases Version 1.9.0_ - 2019-06-19 --------------------------- Features ^^^^^^^^ - implement artifacts deletion - add endpoint to get the variables of a pipeline - delete ProjectPipeline - implement __eq__ and __hash__ methods - Allow runpy invocation of CLI tool (python -m gitlab) - add project releases api - merged new release & registry apis Bug Fixes ^^^^^^^^^ - convert # to %23 in URLs - pep8 errors - use python2 compatible syntax for super - Make MemberManager.all() return a list of objects - %d replaced by %s - Re-enable command specific help messages - dont ask for id attr if this is *Manager originating custom action - fix -/_ replacament for *Manager custom actions - fix repository_id marshaling in cli - register cli action for delete_in_bulk Version 1.8.0_ - 2019-02-22 --------------------------- * docs(setup): use proper readme on PyPI * docs(readme): provide commit message guidelines * fix(api): make reset_time_estimate() work again * fix: handle empty 'Retry-After' header from GitLab * fix: remove decode() on error_message string * chore: release tags to PyPI automatically * fix(api): avoid parameter conflicts with python and gitlab * fix(api): Don't try to parse raw downloads * feat: Added approve & unapprove method for Mergerequests * fix all kwarg behaviour Version 1.7.0_ - 2018-12-09 --------------------------- * [docs] Fix the owned/starred usage documentation * [docs] Add a warning about http to https redirects * Fix the https redirection test * [docs] Add a note about GroupProject limited API * Add missing comma in ProjectIssueManager _create_attrs * More flexible docker image * Add project protected tags management * [cli] Print help and usage without config file * Rename MASTER_ACCESS to MAINTAINER_ACCESS * [docs] Add docs build information * Use docker image with current sources * [docs] Add PyYAML requirement notice * Add Gitter badge to README * [docs] Add an example of pipeline schedule vars listing * [cli] Exit on config parse error, instead of crashing * Add support for resource label events * [docs] Fix the milestone filetring doc (iid -> iids) * [docs] Fix typo in custom attributes example * Improve error message handling in exceptions * Add support for members all() method * Add access control options to protected branch creation Version 1.6.0_ - 2018-08-25 --------------------------- * [docs] Don't use hardcoded values for ids * [docs] Improve the snippets examples * [cli] Output: handle bytes in API responses * [cli] Fix the case where we have nothing to print * Project import: fix the override_params parameter * Support group and global MR listing * Implement MR.pipelines() * MR: add the squash attribute for create/update * Added support for listing forks of a project * [docs] Add/update notes about read-only objects * Raise an exception on https redirects for PUT/POST * [docs] Add a FAQ * [cli] Fix the project-export download Version 1.5.1_ - 2018-06-23 --------------------------- * Fix the ProjectPipelineJob base class (regression) Version 1.5.0_ - 2018-06-22 --------------------------- * Drop API v3 support * Drop GetFromListMixin * Update the sphinx extension for v4 objects * Add support for user avatar upload * Add support for project import/export * Add support for the search API * Add a global per_page config option * Add support for the discussions API * Add support for merged branches deletion * Add support for Project badges * Implement user_agent_detail for snippets * Implement commit.refs() * Add commit.merge_requests() support * Deployment: add list filters * Deploy key: add missing attributes * Add support for environment stop() * Add feature flags deletion support * Update some group attributes * Issues: add missing attributes and methods * Fix the participants() decorator * Add support for group boards * Implement the markdown rendering API * Update MR attributes * Add pipeline listing filters * Add missing project attributes * Implement runner jobs listing * Runners can be created (registered) * Implement runner token validation * Update the settings attributes * Add support for the gitlab CI lint API * Add support for group badges * Fix the IssueManager path to avoid redirections * time_stats(): use an existing attribute if available * Make ProjectCommitStatus.create work with CLI * Tests: default to python 3 * ProjectPipelineJob was defined twice * Silence logs/warnings in unittests * Add support for MR approval configuration (EE) * Change post_data default value to None * Add geo nodes API support (EE) * Add support for issue links (EE) * Add support for LDAP groups (EE) * Add support for board creation/deletion (EE) * Add support for Project.pull_mirror (EE) * Add project push rules configuration (EE) * Add support for the EE license API * Add support for the LDAP groups API (EE) * Add support for epics API (EE) * Fix the non-verbose output of ProjectCommitComment Version 1.4.0_ - 2018-05-19 --------------------------- * Require requests>=2.4.2 * ProjectKeys can be updated * Add support for unsharing projects (v3/v4) * [cli] fix listing for json and yaml output * Fix typos in documentation * Introduce RefreshMixin * [docs] Fix the time tracking examples * [docs] Commits: add an example of binary file creation * [cli] Allow to read args from files * Add support for recursive tree listing * [cli] Restore the --help option behavior * Add basic unit tests for v4 CLI * [cli] Fix listing of strings * Support downloading a single artifact file * Update docs copyright years * Implement attribute types to handle special cases * [docs] fix GitLab reference for notes * Expose additional properties for Gitlab objects * Fix the impersonation token deletion example * feat: obey the rate limit * Fix URL encoding on branch methods * [docs] add a code example for listing commits of a MR * [docs] update service.available() example for API v4 * [tests] fix functional tests for python3 * api-usage: bit more detail for listing with `all` * More efficient .get() for group members * Add docs for the `files` arg in http_* * Deprecate GetFromListMixin 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 projects 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 library 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 an AUTHORS file * cli: support ssl_verify config option * Add ssl_verify option to Gitlab object. Defaults 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.9.0: https://github.com/python-gitlab/python-gitlab/compare/1.8.0...1.9.0 .. _1.8.0: https://github.com/python-gitlab/python-gitlab/compare/1.7.0...1.8.0 .. _1.7.0: https://github.com/python-gitlab/python-gitlab/compare/1.6.0...1.7.0 .. _1.6.0: https://github.com/python-gitlab/python-gitlab/compare/1.5.1...1.6.0 .. _1.5.1: https://github.com/python-gitlab/python-gitlab/compare/1.5.0...1.5.1 .. _1.5.0: https://github.com/python-gitlab/python-gitlab/compare/1.4.0...1.5.0 .. _1.4.0: https://github.com/python-gitlab/python-gitlab/compare/1.3.0...1.4.0 .. _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-2.0.1/Dockerfile000066400000000000000000000006001361651701000162610ustar00rootroot00000000000000FROM python:3.8-alpine AS build WORKDIR /opt/python-gitlab COPY . . RUN python setup.py bdist_wheel FROM python:3.8-alpine WORKDIR /opt/python-gitlab COPY --from=build /opt/python-gitlab/dist dist/ RUN pip install PyYaml RUN pip install $(find dist -name *.whl) && \ rm -rf dist/ COPY docker-entrypoint.sh /usr/local/bin/ ENTRYPOINT ["docker-entrypoint.sh"] CMD ["--version"] python-gitlab-2.0.1/MANIFEST.in000066400000000000000000000004361361651701000160340ustar00rootroot00000000000000include 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 recursive-include gitlab/tests/data * python-gitlab-2.0.1/README.rst000066400000000000000000000110461361651701000157640ustar00rootroot00000000000000.. 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 .. image:: https://img.shields.io/gitter/room/python-gitlab/Lobby.svg :target: https://gitter.im/python-gitlab/Lobby .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/python/black Python GitLab ============= ``python-gitlab`` is a Python package providing access to the GitLab server API. It supports the v4 API of GitLab, and provides a CLI tool (``gitlab``). Installation ============ Requirements ------------ python-gitlab depends on: * `python-requests `_ Install with pip ---------------- .. code-block:: console pip install python-gitlab Using the python-gitlab docker image ==================================== How to build ------------ ``docker build -t python-gitlab:TAG .`` How to use ---------- ``docker run -it --rm -e GITLAB_PRIVATE_TOKEN= -v /path/to/python-gitlab.cfg:/python-gitlab.cfg python-gitlab ...`` or run it directly from the upstream image: ``docker run -it --rm -e GITLAB_PRIVATE_TOKEN= -v /path/to/python-gitlab.cfg:/python-gitlab.cfg registry.gitlab.com/python-gitlab/python-gitlab:latest ...`` To change the GitLab URL, use `-e GITLAB_URL=` Bring your own config file: ``docker run -it --rm -v /path/to/python-gitlab.cfg:/python-gitlab.cfg -e GITLAB_CFG=/python-gitlab.cfg 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 `_. Build the docs -------------- You can build the documentation using ``sphinx``:: pip install sphinx python setup.py build_sphinx 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 We enforce commit messages to be formatted using the `conventional-changelog `_. This leads to more readable messages that are easy to follow when looking through the project history. Please provide your patches as github pull requests. Thanks! Code-Style ---------- We use black as code formatter, so you'll need to format your changes using the `black code formatter `_. Just run .. code-block:: bash cd python-gitlab/ pip3 install --user black black . to format your code according to our guidelines. Running unit tests ------------------ Before submitting a pull request make sure that the tests still succeed with your change. Unit tests and functional tests run using the travis service and passing tests are mandatory to get merge requests accepted. You need to install ``tox`` to run unit tests and documentation builds locally: .. 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-2.0.1/RELEASE_NOTES.rst000066400000000000000000000175201361651701000170620ustar00rootroot00000000000000############# Release notes ############# This page describes important changes between python-gitlab releases. Changes from 1.8 to 1.9 ======================= * ``ProjectMemberManager.all()`` and ``GroupMemberManager.all()`` now return a list of ``ProjectMember`` and ``GroupMember`` objects respectively, instead of a list of dicts. Changes from 1.7 to 1.8 ======================= * You can now use the ``query_parameters`` argument in method calls to define arguments to send to the GitLab server. This allows to avoid conflicts between python-gitlab and GitLab server variables, and allows to use the python reserved keywords as GitLab arguments. The following examples make the same GitLab request with the 2 syntaxes:: projects = gl.projects.list(owned=True, starred=True) projects = gl.projects.list(query_parameters={'owned': True, 'starred': True}) The following example only works with the new parameter:: activities = gl.user_activities.list( query_parameters={'from': '2019-01-01'}, all=True) * Additionally the ``all`` paremeter is not sent to the GitLab anymore. Changes from 1.5 to 1.6 ======================= * When python-gitlab detects HTTP redirections from http to https it will raise a RedirectionError instead of a cryptic error. Make sure to use an ``https://`` protocol in your GitLab URL parameter if the server requires it. Changes from 1.4 to 1.5 ======================= * APIv3 support has been removed. Use the 1.4 release/branch if you need v3 support. * GitLab EE features are now supported: Geo nodes, issue links, LDAP groups, project/group boards, project mirror pulling, project push rules, EE license configuration, epics. * The ``GetFromListMixin`` class has been removed. The ``get()`` method is not available anymore for the following managers: - UserKeyManager - DeployKeyManager - GroupAccessRequestManager - GroupIssueManager - GroupProjectManager - GroupSubgroupManager - IssueManager - ProjectCommitStatusManager - ProjectEnvironmentManager - ProjectLabelManager - ProjectPipelineJobManager - ProjectAccessRequestManager - TodoManager * ``ProjectPipelineJob`` do not heritate from ``ProjectJob`` anymore and thus can only be listed. Changes from 1.3 to 1.4 ======================= * 1.4 is the last release supporting the v3 API, and the related code will be removed in the 1.5 version. If you are using a Gitlab server version that does not support the v4 API you can: * upgrade the server (recommended) * make sure to use version 1.4 of python-gitlab (``pip install python-gitlab==1.4``) See also the `Switching to GitLab API v4 documentation `__. * python-gitlab now handles the server rate limiting feature. It will pause for the required time when reaching the limit (`documentation `__) * The ``GetFromListMixin.get()`` method is deprecated and will be removed in the next python-gitlab version. The goal of this mixin/method is to provide a way to get an object by looping through a list for GitLab objects that don't support the GET method. The method `is broken `__ and conflicts with the GET method now supported by some GitLab objects. You can implement your own method with something like: .. code-block:: python def get_from_list(self, id): for obj in self.list(as_list=False): if obj.get_id() == id: return obj * The ``GroupMemberManager``, ``NamespaceManager`` and ``ProjectBoardManager`` managers now use the GET API from GitLab instead of the ``GetFromListMixin.get()`` method. 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 GitLab 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-2.0.1/docker-entrypoint.sh000077500000000000000000000010771361651701000203170ustar00rootroot00000000000000#!/bin/sh GITLAB_CFG=${GITLAB_CFG:-"/etc/python-gitlab-default.cfg"} cat << EOF > /etc/python-gitlab-default.cfg [global] default = gitlab ssl_verify = ${GITLAB_SSL_VERIFY:-true} timeout = ${GITLAB_TIMEOUT:-5} api_version = ${GITLAB_API_VERSION:-4} per_page = ${GITLAB_PER_PAGE:-10} [gitlab] url = ${GITLAB_URL:-https://gitlab.com} private_token = ${GITLAB_PRIVATE_TOKEN} oauth_token = ${GITLAB_OAUTH_TOKEN} job_token = ${GITLAB_JOB_TOKEN} http_username = ${GITLAB_HTTP_USERNAME} http_password = ${GITLAB_HTTP_PASSWORD} EOF exec gitlab --config-file "${GITLAB_CFG}" "$@" python-gitlab-2.0.1/docs/000077500000000000000000000000001361651701000152235ustar00rootroot00000000000000python-gitlab-2.0.1/docs/Makefile000066400000000000000000000152061361651701000166670ustar00rootroot00000000000000# 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-2.0.1/docs/_templates/000077500000000000000000000000001361651701000173605ustar00rootroot00000000000000python-gitlab-2.0.1/docs/_templates/breadcrumbs.html000066400000000000000000000017471361651701000225500ustar00rootroot00000000000000{# 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-2.0.1/docs/api-objects.rst000066400000000000000000000020611361651701000201540ustar00rootroot00000000000000############ API examples ############ .. toctree:: :maxdepth: 1 gl_objects/access_requests gl_objects/appearance gl_objects/emojis gl_objects/badges gl_objects/branches gl_objects/clusters gl_objects/messages gl_objects/commits gl_objects/deploy_keys gl_objects/deployments gl_objects/discussions gl_objects/environments gl_objects/events gl_objects/epics gl_objects/features gl_objects/geo_nodes gl_objects/groups gl_objects/issues gl_objects/boards gl_objects/labels gl_objects/notifications gl_objects/mrs gl_objects/mr_approvals gl_objects/milestones gl_objects/namespaces gl_objects/notes gl_objects/pagesdomains gl_objects/pipelines_and_jobs gl_objects/projects gl_objects/protected_branches gl_objects/runners gl_objects/repositories gl_objects/repository_tags gl_objects/search 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-2.0.1/docs/api-usage.rst000066400000000000000000000271351361651701000176400ustar00rootroot00000000000000############################ Getting started with the API ############################ python-gitlab only supports GitLab APIs v4. ``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') # job token authentication (to be used in CI) import os gl = gitlab.Gitlab('http://10.0.0.1', job_token=os.environ['CI_JOB_TOKEN']) # 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. .. warning:: If the GitLab server you are using redirects requests from http to https, make sure to use the ``https://`` protocol in the URL definition. 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 preferred 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. 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 project in group.projects.list(): print(project) # 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 and update with the manager's ``get_create_attrs()`` and ``get_update_attrs()`` methods. They return 2 tuples, the first one is the list of mandatory attributes, the second one is 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 ``attributes`` attribute: .. code-block:: python project = gl.projects.get(1) 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() python-gitlab allows to send any data to the GitLab server when making queries. In case of invalid or missing arguments python-gitlab will raise an exception with the GitLab server error message: .. code-block:: python >>> gl.projects.list(sort='invalid value') ... GitlabListError: 400: sort does not have a valid value You can use the ``query_parameters`` argument to send arguments that would conflict with python or python-gitlab when using them as kwargs: .. code-block:: python gl.user_activities.list(from='2019-01-01') ## invalid gl.user_activities.list(query_parameters={'from': '2019-01-01'}) # OK 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. * ``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. Lazy objects ============ To avoid useless API calls to the server you can create lazy objects. These objects are created locally using a known ID, and give access to other managers and methods. The following example will only make one API call to the GitLab server to star a project (the previous example used 2 API calls): .. 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) .. warning:: The first page is page 1, not page 0. 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.list(owned=True, all=True) You can define the ``per_page`` value globally to avoid passing it to every ``list()`` method call: .. code-block:: python gl = gitlab.Gitlab(url, token, per_page=50) Gitlab allows to also use keyset pagination. You can supply it to your project listing, but you can also do so globally. Be aware that GitLab then also requires you to only use supported order options. At the time of writing, only ``order_by="id"`` works. .. code-block:: python gl = gitlab.Gitlab(url, token, pagination="keyset", order_by="id", per_page=100) gl.projects.list() Reference: https://docs.gitlab.com/ce/api/README.html#keyset-based-pagination ``list()`` methods can also return a generator object which will handle the next calls to the API when required. This is the recommended way to iterate through a large number of items: .. 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 from 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 the ``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() session.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 Rate limits ----------- python-gitlab obeys the rate limit of the GitLab server by default. On receiving a 429 response (Too Many Requests), python-gitlab sleeps for the amount of time in the Retry-After header that GitLab sends back. If GitLab does not return a response with the Retry-After header, python-gitlab will perform an exponential backoff. If you don't want to wait, you can disable the rate-limiting feature, by supplying the ``obey_rate_limit`` argument. .. code-block:: python import gitlab import requests gl = gitlab.gitlab(url, token, api_version=4) gl.projects.list(all=True, obey_rate_limit=False) If you do not disable the rate-limiting feature, you can supply a custom value for ``max_retries``; by default, this is set to 10. To retry without bound when throttled, you can set this parameter to -1. This parameter is ignored if ``obey_rate_limit`` is set to ``False``. .. code-block:: python import gitlab import requests gl = gitlab.gitlab(url, token, api_version=4) gl.projects.list(all=True, max_retries=12) .. warning:: You will get an Exception, if you then go over the rate limit of your GitLab instance. Transient errors ---------------- GitLab server can sometimes return a transient HTTP error. python-gitlab can automatically retry in such case, when ``retry_transient_errors`` argument is set to ``True``. When enabled, HTTP error codes 500 (Internal Server Error), 502 (502 Bad Gateway), 503 (Service Unavailable), and 504 (Gateway Timeout) are retried. By default an exception is raised for these errors. .. code-block:: python import gitlab import requests gl = gitlab.gitlab(url, token, api_version=4) gl.projects.list(all=True, retry_transient_errors=True) Timeout ------- python-gitlab will by default use the ``timeout`` option from it's configuration for all requests. This is passed downwards to the ``requests`` module at the time of making the HTTP request. However if you would like to override the global timeout parameter for a particular call, you can provide the ``timeout`` parameter to that API invocation: .. code-block:: python import gitlab gl = gitlab.gitlab(url, token, api_version=4) gl.projects.import_github(ACCESS_TOKEN, 123456, "root", timeout=120.0) python-gitlab-2.0.1/docs/api/000077500000000000000000000000001361651701000157745ustar00rootroot00000000000000python-gitlab-2.0.1/docs/api/gitlab.rst000066400000000000000000000021521361651701000177700ustar00rootroot00000000000000gitlab package ============== Subpackages ----------- .. toctree:: 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-2.0.1/docs/api/gitlab.v4.rst000066400000000000000000000005001361651701000203130ustar00rootroot00000000000000gitlab.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-2.0.1/docs/changelog.rst000066400000000000000000000000361361651701000177030ustar00rootroot00000000000000.. include:: ../ChangeLog.rst python-gitlab-2.0.1/docs/cli.rst000066400000000000000000000204651361651701000165330ustar00rootroot00000000000000#################### ``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 = 4 [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`` - ``4`` - The API version to use to make queries. Only ``4`` is available since 1.5.0. * - ``per_page`` - Integer between 1 and 100 - The number of items to return in listing queries. GitLab limits the value at 100. You must define the ``url`` in each GitLab server section. .. warning:: If the GitLab server you are using redirects requests from http to https, make sure to use the ``https://`` protocol in the ``url`` definition. Only one of ``private_token``, ``oauth_token`` or ``job_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`_pat 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. * - ``job_token`` - Your job token. See `the official documentation`_job-token to learn how to obtain a token. * - ``api_version`` - GitLab API version to use. Only ``4`` is available since 1.5.0. * - ``http_username`` - Username for optional HTTP authentication * - ``http_password`` - Password for optional HTTP authentication .. _pat: https://docs.gitlab.com/ce/user/profile/personal_access_tokens.html .. _job-token: https://docs.gitlab.com/ce/api/jobs.html#get-job-artifacts 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``. **Notice:** The `PyYAML package `_ is required to use the yaml output option. You need to install it explicitly using ``pip install python-gitlab[yaml]`` ``--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 List values are comma-separated: .. code-block:: console $ gitlab issue list --labels foo,bar Reading values from files ------------------------- You can make ``gitlab`` read values from files instead of providing them on the command line. This is handy for values containing new lines for instance: .. code-block:: console $ cat > /tmp/description << EOF This is the description of my project. It is obviously the best project around EOF $ gitlab project create --name SuperProject --description @/tmp/description Enabling shell autocompletion ============================ To get autocompletion, you'll need to install the package with the extra "autocompletion": .. code-block:: console pip install python_gitlab[autocompletion] Add the appropriate command below to your shell's config file so that it is run on startup. You will likely have to restart or re-login for the autocompletion to start working. Bash ---- .. code-block:: console eval "$(register-python-argcomplete gitlab)" tcsh ---- .. code-block:: console eval `register-python-argcomplete --shell tcsh gitlab` fish ---- .. code-block:: console register-python-argcomplete --shell fish gitlab | . Zsh --- .. warning:: Zsh autocompletion support is broken right now in the argcomplete python package. Perhaps it will be fixed in a future release of argcomplete at which point the following instructions will enable autocompletion in zsh. To activate completions for zsh you need to have bashcompinit enabled in zsh: .. code-block:: console autoload -U bashcompinit bashcompinit Afterwards you can enable completion for gitlab: .. code-block:: console eval "$(register-python-argcomplete gitlab)" python-gitlab-2.0.1/docs/conf.py000066400000000000000000000215721361651701000165310ustar00rootroot00000000000000#!/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-2018, 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-2.0.1/docs/ext/000077500000000000000000000000001361651701000160235ustar00rootroot00000000000000python-gitlab-2.0.1/docs/ext/__init__.py000066400000000000000000000000001361651701000201220ustar00rootroot00000000000000python-gitlab-2.0.1/docs/ext/docstrings.py000066400000000000000000000034701361651701000205600ustar00rootroot00000000000000import inspect import itertools import os import jinja2 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 conf.items(): 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.startswith("gitlab.v4.objects") and name.endswith("Manager"): self._parsed_lines.extend(self._build_doc("manager_tmpl.j2", cls=self._obj)) python-gitlab-2.0.1/docs/ext/manager_tmpl.j2000066400000000000000000000014601361651701000207270ustar00rootroot00000000000000{% if cls._list_filters %} **Object listing filters** {% for item in cls._list_filters %} - ``{{ item }}`` {% endfor %} {% endif %} {% if cls._create_attrs %} **Object Creation** {% if cls._create_attrs[0] %} Mandatory attributes: {% for item in cls._create_attrs[0] %} - ``{{ item }}`` {% endfor %} {% endif %} {% if cls._create_attrs[1] %} Optional attributes: {% for item in cls._create_attrs[1] %} - ``{{ item }}`` {% endfor %} {% endif %} {% endif %} {% if cls._update_attrs %} **Object update** {% if cls._update_attrs[0] %} Mandatory attributes for object update: {% for item in cls._update_attrs[0] %} - ``{{ item }}`` {% endfor %} {% endif %} {% if cls._update_attrs[1] %} Optional attributes for object update: {% for item in cls._update_attrs[1] %} - ``{{ item }}`` {% endfor %} {% endif %} {% endif %} python-gitlab-2.0.1/docs/faq.rst000066400000000000000000000022631361651701000165270ustar00rootroot00000000000000### FAQ ### I cannot edit the merge request / issue I've just retrieved It is likely that you used a ``MergeRequest``, ``GroupMergeRequest``, ``Issue`` or ``GroupIssue`` object. These objects cannot be edited. But you can create a new ``ProjectMergeRequest`` or ``ProjectIssue`` object to apply changes. For example:: issue = gl.issues.list()[0] project = gl.projects.get(issue.project_id, lazy=True) editable_issue = project.issues.get(issue.iid, lazy=True) # you can now edit the object See the :ref:`merge requests example ` and the :ref:`issues examples `. How can I clone the repository of a project? python-gitlab doesn't provide an API to clone a project. You have to use a git library or call the ``git`` command. The git URI is exposed in the ``ssh_url_to_repo`` attribute of ``Project`` objects. Example:: import subprocess project = gl.projects.create(data) # or gl.projects.get(project_id) print(project.attributes) # displays all the attributes git_url = project.ssh_url_to_repo subprocess.call(['git', 'clone', git_url]) python-gitlab-2.0.1/docs/gl_objects/000077500000000000000000000000001361651701000173365ustar00rootroot00000000000000python-gitlab-2.0.1/docs/gl_objects/access_requests.rst000066400000000000000000000026211361651701000232650ustar00rootroot00000000000000############### 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.MAINTAINER_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` * GitLab API: https://docs.gitlab.com/ce/api/access_requests.html Examples -------- List access requests from projects and groups:: p_ars = project.accessrequests.list() g_ars = group.accessrequests.list() Create an access request:: p_ar = project.accessrequests.create({}) g_ar = group.accessrequests.create({}) Approve an access request:: ar.approve() # defaults to DEVELOPER level ar.approve(access_level=gitlab.MAINTAINER_ACCESS) # explicitly set access level Deny (delete) an access request:: project.accessrequests.delete(user_id) group.accessrequests.delete(user_id) # or ar.delete() python-gitlab-2.0.1/docs/gl_objects/appearance.rst000066400000000000000000000006631361651701000221740ustar00rootroot00000000000000########## Appearance ########## Reference --------- * v4 API: + :class:`gitlab.v4.objects.ApplicationAppearance` + :class:`gitlab.v4.objects.ApplicationAppearanceManager` + :attr:`gitlab.Gitlab.appearance` * GitLab API: https://docs.gitlab.com/ce/api/appearance.html Examples -------- Get the appearance:: appearance = gl.appearance.get() Update the appearance:: appearance.title = "Test" appearance.save() python-gitlab-2.0.1/docs/gl_objects/badges.rst000066400000000000000000000020331361651701000213130ustar00rootroot00000000000000###### Badges ###### Badges can be associated with groups and projects. Reference --------- * v4 API: + :class:`gitlab.v4.objects.GroupBadge` + :class:`gitlab.v4.objects.GroupBadgeManager` + :attr:`gitlab.v4.objects.Group.badges` + :class:`gitlab.v4.objects.ProjectBadge` + :class:`gitlab.v4.objects.ProjectBadgeManager` + :attr:`gitlab.v4.objects.Project.badges` * GitLab API: + https://docs.gitlab.com/ce/api/group_badges.html + https://docs.gitlab.com/ce/api/project_badges.html Examples -------- List badges:: badges = group_or_project.badges.list() Get ad badge:: badge = group_or_project.badges.get(badge_id) Create a badge:: badge = group_or_project.badges.create({'link_url': link, 'image_url': image_link}) Update a badge:: badge.image_link = new_link badge.save() Delete a badge:: badge.delete() Render a badge (preview the generate URLs):: output = group_or_project.badges.render(link, image_link) print(output['rendered_link_url']) print(output['rendered_image_url']) python-gitlab-2.0.1/docs/gl_objects/boards.rst000066400000000000000000000045511361651701000213470ustar00rootroot00000000000000############ Issue boards ############ Boards ====== Boards are a visual representation of existing issues for a project or a group. 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` + :class:`gitlab.v4.objects.GroupBoard` + :class:`gitlab.v4.objects.GroupBoardManager` + :attr:`gitlab.v4.objects.Group.boards` * GitLab API: + https://docs.gitlab.com/ce/api/boards.html + https://docs.gitlab.com/ce/api/group_boards.html Examples -------- Get the list of existing boards for a project or a group:: # item is a Project or a Group boards = project_or_group.boards.list() Get a single board for a project or a group:: board = project_or_group.boards.get(board_id) Create a board:: board = project_or_group.boards.create({'name': 'new-board'}) .. note:: Board creation is not supported in the GitLab CE edition. Delete a board:: board.delete() # or project_or_group.boards.delete(board_id) .. note:: Board deletion is not supported in the GitLab CE edition. Board lists =========== Boards are made of lists of issues. Each list is associated to a label, and issues tagged with this label automatically belong to the list. Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectBoardList` + :class:`gitlab.v4.objects.ProjectBoardListManager` + :attr:`gitlab.v4.objects.ProjectBoard.lists` + :class:`gitlab.v4.objects.GroupBoardList` + :class:`gitlab.v4.objects.GroupBoardListManager` + :attr:`gitlab.v4.objects.GroupBoard.lists` * GitLab API: + https://docs.gitlab.com/ce/api/boards.html + https://docs.gitlab.com/ce/api/group_boards.html Examples -------- List the issue lists for a board:: b_lists = board.lists.list() Get a single list:: b_list = board.lists.get(list_id) Create a new list:: # 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}) 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:: b_list.position = 2 b_list.save() Delete a list:: b_list.delete() python-gitlab-2.0.1/docs/gl_objects/branches.rst000066400000000000000000000022031361651701000216520ustar00rootroot00000000000000######## Branches ######## References ---------- * v4 API: + :class:`gitlab.v4.objects.ProjectBranch` + :class:`gitlab.v4.objects.ProjectBranchManager` + :attr:`gitlab.v4.objects.Project.branches` * GitLab API: https://docs.gitlab.com/ce/api/branches.html Examples -------- Get the list of branches for a repository:: branches = project.branches.list() Get a single repository branch:: branch = project.branches.get('master') Create a repository branch:: branch = project.branches.create({'branch': 'feature1', 'ref': 'master'}) Delete a repository branch:: project.branches.delete('feature1') # or branch.delete() Protect/unprotect a repository branch:: branch.protect() branch.unprotect() .. 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) Delete the merged branches for a project:: project.delete_merged_branches() python-gitlab-2.0.1/docs/gl_objects/clusters.rst000066400000000000000000000033751361651701000217440ustar00rootroot00000000000000############ Clusters ############ Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectCluster` + :class:`gitlab.v4.objects.ProjectClusterManager` + :attr:`gitlab.v4.objects.Project.clusters` + :class:`gitlab.v4.objects.GroupCluster` + :class:`gitlab.v4.objects.GroupClusterManager` + :attr:`gitlab.v4.objects.Group.clusters` * GitLab API: https://docs.gitlab.com/ee/api/project_clusters.html * GitLab API: https://docs.gitlab.com/ee/api/group_clusters.html Examples -------- List clusters for a project:: clusters = project.clusters.list() Create an cluster for a project:: cluster = project.clusters.create( { "name": "cluster1", "platform_kubernetes_attributes": { "api_url": "http://url", "token": "tokenval", }, }) Retrieve a specific cluster for a project:: cluster = project.clusters.get(cluster_id) Update an cluster for a project:: cluster.platform_kubernetes_attributes = {"api_url": "http://newurl"} cluster.save() Delete an cluster for a project:: cluster = project.clusters.delete(cluster_id) # or cluster.delete() List clusters for a group:: clusters = group.clusters.list() Create an cluster for a group:: cluster = group.clusters.create( { "name": "cluster1", "platform_kubernetes_attributes": { "api_url": "http://url", "token": "tokenval", }, }) Retrieve a specific cluster for a group:: cluster = group.clusters.get(cluster_id) Update an cluster for a group:: cluster.platform_kubernetes_attributes = {"api_url": "http://newurl"} cluster.save() Delete an cluster for a group:: cluster = group.clusters.delete(cluster_id) # or cluster.delete() python-gitlab-2.0.1/docs/gl_objects/commits.rst000066400000000000000000000063351361651701000215520ustar00rootroot00000000000000####### Commits ####### Commits ======= Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectCommit` + :class:`gitlab.v4.objects.ProjectCommitManager` + :attr:`gitlab.v4.objects.Project.commits` Examples -------- List the commits for a project:: commits = project.commits.list() You can use the ``ref_name``, ``since`` and ``until`` filters to limit the results:: commits = project.commits.list(ref_name='my_branch') commits = project.commits.list(since='2016-01-01T00:00:00Z') .. note:: The available ``all`` listing argument conflicts with the python-gitlab argument. Use ``query_parameters`` to avoid the conflict:: commits = project.commits.list(all=True, query_parameters={'ref_name': 'my_branch'}) Create a commit:: # See https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions # for actions detail data = { 'branch': 'master', 'commit_message': 'blah blah blah', 'actions': [ { 'action': 'create', 'file_path': 'README.rst', 'content': open('path/to/file.rst').read(), }, { # Binary files need to be base64 encoded 'action': 'create', 'file_path': 'logo.png', 'content': base64.b64encode(open('logo.png').read()), 'encoding': 'base64', } ] } commit = project.commits.create(data) Get a commit detail:: commit = project.commits.get('e3d5a71b') Get the diff for a commit:: diff = commit.diff() Cherry-pick a commit into another branch:: commit.cherry_pick(branch='target_branch') Get the references the commit has been pushed to (branches and tags):: commit.refs() # all references commit.refs('tag') # only tags commit.refs('branch') # only branches List the merge requests related to a commit:: commit.merge_requests() Commit comments =============== Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectCommitComment` + :class:`gitlab.v4.objects.ProjectCommitCommentManager` + :attr:`gitlab.v4.objects.ProjectCommit.comments` * GitLab API: https://docs.gitlab.com/ce/api/commits.html Examples -------- Get the comments for a commit:: comments = commit.comments.list() Add a comment on a commit:: # 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'}) Commit status ============= Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectCommitStatus` + :class:`gitlab.v4.objects.ProjectCommitStatusManager` + :attr:`gitlab.v4.objects.ProjectCommit.statuses` * GitLab API: https://docs.gitlab.com/ce/api/commits.html Examples -------- List the statuses for a commit:: statuses = commit.statuses.list() Change the status of a commit:: commit.statuses.create({'state': 'success'}) python-gitlab-2.0.1/docs/gl_objects/deploy_keys.rst000066400000000000000000000023301361651701000224150ustar00rootroot00000000000000########### Deploy keys ########### Deploy keys =========== Reference --------- * v4 API: + :class:`gitlab.v4.objects.DeployKey` + :class:`gitlab.v4.objects.DeployKeyManager` + :attr:`gitlab.Gitlab.deploykeys` * GitLab API: https://docs.gitlab.com/ce/api/deploy_keys.html Examples -------- List the deploy keys:: keys = gl.deploykeys.list() 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` * GitLab API: https://docs.gitlab.com/ce/api/deploy_keys.html Examples -------- List keys for a project:: keys = project.keys.list() Get a single deploy key:: key = project.keys.get(key_id) Create a deploy key for a project:: key = project.keys.create({'title': 'jenkins key', 'key': open('/home/me/.ssh/id_rsa.pub').read()}) Delete a deploy key for a project:: key = project.keys.list(key_id) # or key.delete() Enable a deploy key for a project:: project.keys.enable(key_id) Disable a deploy key for a project:: project_key.delete() python-gitlab-2.0.1/docs/gl_objects/deployments.rst000066400000000000000000000014501361651701000224330ustar00rootroot00000000000000########### Deployments ########### Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectDeployment` + :class:`gitlab.v4.objects.ProjectDeploymentManager` + :attr:`gitlab.v4.objects.Project.deployments` * GitLab API: https://docs.gitlab.com/ce/api/deployments.html Examples -------- List deployments for a project:: deployments = project.deployments.list() Get a single deployment:: deployment = project.deployments.get(deployment_id) Create a new deployment:: deployment = project.deployments.create({ "environment": "Test", "sha": "1agf4gs", "ref": "master", "tag": False, "status": "created", }) Update a deployment:: deployment = project.deployments.get(42) deployment.status = "failed" deployment.save() python-gitlab-2.0.1/docs/gl_objects/discussions.rst000066400000000000000000000065461361651701000224510ustar00rootroot00000000000000########### Discussions ########### Discussions organize the notes in threads. See the :ref:`project-notes` chapter for more information about notes. Discussions are available for project issues, merge requests, snippets and commits. Reference ========= * v4 API: Issues: + :class:`gitlab.v4.objects.ProjectIssueDiscussion` + :class:`gitlab.v4.objects.ProjectIssueDiscussionManager` + :class:`gitlab.v4.objects.ProjectIssueDiscussionNote` + :class:`gitlab.v4.objects.ProjectIssueDiscussionNoteManager` + :attr:`gitlab.v4.objects.ProjectIssue.notes` MergeRequests: + :class:`gitlab.v4.objects.ProjectMergeRequestDiscussion` + :class:`gitlab.v4.objects.ProjectMergeRequestDiscussionManager` + :class:`gitlab.v4.objects.ProjectMergeRequestDiscussionNote` + :class:`gitlab.v4.objects.ProjectMergeRequestDiscussionNoteManager` + :attr:`gitlab.v4.objects.ProjectMergeRequest.notes` Snippets: + :class:`gitlab.v4.objects.ProjectSnippetDiscussion` + :class:`gitlab.v4.objects.ProjectSnippetDiscussionManager` + :class:`gitlab.v4.objects.ProjectSnippetDiscussionNote` + :class:`gitlab.v4.objects.ProjectSnippetDiscussionNoteManager` + :attr:`gitlab.v4.objects.ProjectSnippet.notes` * GitLab API: https://docs.gitlab.com/ce/api/discussions.html Examples ======== List the discussions for a resource (issue, merge request, snippet or commit):: discussions = resource.discussions.list() Get a single discussion:: discussion = resource.discussions.get(discussion_id) You can access the individual notes in the discussion through the ``notes`` attribute. It holds a list of notes in chronological order:: # ``resource.notes`` is a DiscussionNoteManager, so we need to get the # object notes using ``attributes`` for note in discussion.attributes['notes']: print(note['body']) .. note:: The notes are dicts, not objects. You can add notes to existing discussions:: new_note = discussion.notes.create({'body': 'Episode IV: A new note'}) You can get and update a single note using the ``*DiscussionNote`` resources:: discussion = resource.discussions.get(discussion_id) # Get the latest note's id note_id = discussion.attributes['note'][-1]['id'] last_note = discussion.notes.get(note_id) last_note.body = 'Updated comment' last_note.save() Create a new discussion:: discussion = resource.discussions.create({'body': 'First comment of discussion'}) You can comment on merge requests and commit diffs. Provide the ``position`` dict to define where the comment should appear in the diff:: mr_diff = mr.diffs.get(diff_id) mr.discussions.create({'body': 'Note content', 'position': { 'base_sha': mr_diff.base_commit_sha, 'start_sha': mr_diff.start_commit_sha, 'head_sha': mr_diff.head_commit_sha, 'position_type': 'text', 'new_line': 1, 'old_path': 'README.rst', 'new_path': 'README.rst'} }) Resolve / unresolve a merge request discussion:: mr_d = mr.discussions.get(d_id) mr_d.resolved = True # True to resolve, False to unresolve mr_d.save() Delete a comment:: discussions.notes.delete(note_id) # or note.delete() python-gitlab-2.0.1/docs/gl_objects/emojis.rst000066400000000000000000000022541361651701000213610ustar00rootroot00000000000000############ 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-2.0.1/docs/gl_objects/environments.rst000066400000000000000000000016071361651701000226230ustar00rootroot00000000000000############ Environments ############ Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectEnvironment` + :class:`gitlab.v4.objects.ProjectEnvironmentManager` + :attr:`gitlab.v4.objects.Project.environments` * GitLab API: https://docs.gitlab.com/ce/api/environments.html Examples -------- List environments for a project:: environments = project.environments.list() Create an environment for a project:: environment = project.environments.create({'name': 'production'}) Retrieve a specific environment for a project:: environment = project.environments.get(112) Update an environment for a project:: environment.external_url = 'http://foo.bar.com' environment.save() Delete an environment for a project:: environment = project.environments.delete(environment_id) # or environment.delete() Stop an environments:: environment.stop() python-gitlab-2.0.1/docs/gl_objects/epics.rst000066400000000000000000000024261361651701000211770ustar00rootroot00000000000000##### Epics ##### Epics ===== Reference --------- * v4 API: + :class:`gitlab.v4.objects.GroupEpic` + :class:`gitlab.v4.objects.GroupEpicManager` + :attr:`gitlab.Gitlab.Group.epics` * GitLab API: https://docs.gitlab.com/ee/api/epics.html (EE feature) Examples -------- List the epics for a group:: epics = groups.epics.list() Get a single epic for a group:: epic = group.epics.get(epic_iid) Create an epic for a group:: epic = group.epics.create({'title': 'My Epic'}) Edit an epic:: epic.title = 'New title' epic.labels = ['label1', 'label2'] epic.save() Delete an epic:: epic.delete() Epics issues ============ Reference --------- * v4 API: + :class:`gitlab.v4.objects.GroupEpicIssue` + :class:`gitlab.v4.objects.GroupEpicIssueManager` + :attr:`gitlab.Gitlab.GroupEpic.issues` * GitLab API: https://docs.gitlab.com/ee/api/epic_issues.html (EE feature) Examples -------- List the issues associated with an issue:: ei = epic.issues.list() Associate an issue with an epic:: # use the issue id, not its iid ei = epic.issues.create({'issue_id': 4}) Move an issue in the list:: ei.move_before_id = epic_issue_id_1 # or ei.move_after_id = epic_issue_id_2 ei.save() Delete an issue association:: ei.delete() python-gitlab-2.0.1/docs/gl_objects/events.rst000066400000000000000000000020271361651701000213750ustar00rootroot00000000000000###### 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` * 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 possible 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-2.0.1/docs/gl_objects/features.rst000066400000000000000000000007611361651701000217120ustar00rootroot00000000000000############## 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) Delete a feature:: feature.delete() python-gitlab-2.0.1/docs/gl_objects/geo_nodes.rst000066400000000000000000000013161361651701000220330ustar00rootroot00000000000000######### Geo nodes ######### Reference --------- * v4 API: + :class:`gitlab.v4.objects.GeoNode` + :class:`gitlab.v4.objects.GeoNodeManager` + :attr:`gitlab.Gitlab.geonodes` * GitLab API: https://docs.gitlab.com/ee/api/geo_nodes.html (EE feature) Examples -------- List the geo nodes:: nodes = gl.geonodes.list() Get the status of all the nodes:: status = gl.geonodes.status() Get a specific node and its status:: node = gl.geonodes.get(node_id) node.status() Edit a node configuration:: node.url = 'https://secondary.mygitlab.domain' node.save() Delete a node:: node.delete() List the sync failure on the current node:: failures = gl.geonodes.current_failures() python-gitlab-2.0.1/docs/gl_objects/groups.rst000066400000000000000000000113341361651701000214110ustar00rootroot00000000000000###### Groups ###### Groups ====== Reference --------- * v4 API: + :class:`gitlab.v4.objects.Group` + :class:`gitlab.v4.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() .. note:: ``GroupProject`` objects returned by this API call are very limited, and do not provide all the features of ``Project`` objects. If you need to manipulate projects, create a new ``Project`` object:: first_group_project = group.projects.list()[0] manageable_project = gl.projects.get(first_group_project.id, lazy=True) 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 * ``include_subgroups``: include projects in subgroups 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.groups.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() .. note:: 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.MAINTAINER_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` * GitLab API: https://docs.gitlab.com/ce/api/groups.html Examples -------- List group members:: members = group.members.list() List the group members recursively (including inherited members through ancestor groups):: members = group.members.all(all=True) 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() LDAP group links ================ Add an LDAP group link to an existing GitLab group:: group.add_ldap_group_link(ldap_group_cn, gitlab.DEVELOPER_ACCESS, 'ldapmain') Remove a link:: group.delete_ldap_group_link(ldap_group_cn, 'ldapmain') Sync the LDAP groups:: group.ldap_sync() You can use the ``ldapgroups`` manager to list available LDAP groups:: # listing (supports pagination) ldap_groups = gl.ldapgroups.list() # filter using a group name ldap_groups = gl.ldapgroups.list(search='foo') # list the groups for a specific LDAP provider ldap_groups = gl.ldapgroups.list(search='foo', provider='ldapmain') python-gitlab-2.0.1/docs/gl_objects/issues.rst000066400000000000000000000114251361651701000214060ustar00rootroot00000000000000.. _issues_examples: ###### Issues ###### Reported issues =============== Reference --------- * v4 API: + :class:`gitlab.v4.objects.Issue` + :class:`gitlab.v4.objects.IssueManager` + :attr:`gitlab.Gitlab.issues` * GitLab API: https://docs.gitlab.com/ce/api/issues.html Examples -------- List the issues:: issues = gl.issues.list() Use the ``state`` and ``label`` parameters to filter the results. Use the ``order_by`` and ``sort`` attributes to sort the results:: open_issues = gl.issues.list(state='opened') closed_issues = gl.issues.list(state='closed') tagged_issues = gl.issues.list(labels=['foo', 'bar']) .. note:: It is not possible to edit or delete Issue objects. You need to create a ProjectIssue object to perform changes:: issue = gl.issues.list()[0] project = gl.projects.get(issue.project_id, lazy=True) editable_issue = project.issues.get(issue.iid, lazy=True) editable_issue.title = updated_title editable_issue.save() Group issues ============ Reference --------- * v4 API: + :class:`gitlab.v4.objects.GroupIssue` + :class:`gitlab.v4.objects.GroupIssueManager` + :attr:`gitlab.v4.objects.Group.issues` * GitLab API: https://docs.gitlab.com/ce/api/issues.html Examples -------- List the group issues:: 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') .. note:: It is not possible to edit or delete GroupIssue objects. You need to create a ProjectIssue object to perform changes:: issue = group.issues.list()[0] project = gl.projects.get(issue.project_id, lazy=True) editable_issue = project.issues.get(issue.iid, lazy=True) editable_issue.title = updated_title editable_issue.save() Project issues ============== Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectIssue` + :class:`gitlab.v4.objects.ProjectIssueManager` + :attr:`gitlab.v4.objects.Project.issues` * GitLab API: https://docs.gitlab.com/ce/api/issues.html Examples -------- List the project issues:: 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') Get a project issue:: issue = project.issues.get(issue_iid) Create a new issue:: issue = project.issues.create({'title': 'I have a bug', 'description': 'Something useful here.'}) Update an issue:: issue.labels = ['foo', 'bar'] issue.save() Close / reopen an issue:: # close an issue issue.state_event = 'close' issue.save() # reopen it issue.state_event = 'reopen' issue.save() Delete an issue:: project.issues.delete(issue_id) # pr issue.delete() Subscribe / unsubscribe from an issue:: issue.subscribe() issue.unsubscribe() Move an issue to another project:: issue.move(other_project_id) Make an issue as todo:: issue.todo() Get time tracking stats:: issue.time_stats() On recent versions of Gitlab the time stats are also returned as an issue object attribute:: issue = project.issue.get(iid) print(issue.attributes['time_stats']) Set a time estimate for an issue:: issue.time_estimate('3h30m') Reset a time estimate for an issue:: issue.reset_time_estimate() Add spent time for an issue:: issue.add_spent_time('3h30m') Reset spent time for an issue:: issue.reset_spent_time() Get user agent detail for the issue (admin only):: detail = issue.user_agent_detail() Get the list of merge requests that will close an issue when merged:: mrs = issue.closed_by() Get the merge requests related to an issue:: mrs = issue.related_merge_requests() Get the list of participants:: users = issue.participants() Issue links =========== Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectIssueLink` + :class:`gitlab.v4.objects.ProjectIssueLinkManager` + :attr:`gitlab.v4.objects.ProjectIssue.links` * GitLab API: https://docs.gitlab.com/ee/api/issue_links.html (EE feature) Examples -------- List the issues linked to ``i1``:: links = i1.links.list() Link issue ``i1`` to issue ``i2``:: data = { 'target_project_id': i2.project_id, 'target_issue_iid': i2.iid } src_issue, dest_issue = i1.links.create(data) .. note:: The ``create()`` method returns the source and destination ``ProjectIssue`` objects, not a ``ProjectIssueLink`` object. Delete a link:: i1.links.delete(issue_link_id) python-gitlab-2.0.1/docs/gl_objects/labels.rst000066400000000000000000000042631361651701000213370ustar00rootroot00000000000000###### Labels ###### Project labels ============== Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectLabel` + :class:`gitlab.v4.objects.ProjectLabelManager` + :attr:`gitlab.v4.objects.Project.labels` * GitLab API: https://docs.gitlab.com/ce/api/labels.html Examples -------- List labels for a project:: labels = project.labels.list() Create a label for a project:: label = project.labels.create({'name': 'foo', 'color': '#8899aa'}) Update a label for a project:: # change the name of the label: label.new_name = 'bar' label.save() # change its color: label.color = '#112233' label.save() Delete a label for a project:: project.labels.delete(label_id) # or label.delete() Manage labels in issues and merge requests:: # 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() Label events ============ Resource label events keep track about who, when, and which label was added or removed to an issuable. Group epic label events are only available in the EE edition. Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectIssueResourceLabelEvent` + :class:`gitlab.v4.objects.ProjectIssueResourceLabelEventManager` + :attr:`gitlab.v4.objects.ProjectIssue.resourcelabelevents` + :class:`gitlab.v4.objects.ProjectMergeRequestResourceLabelEvent` + :class:`gitlab.v4.objects.ProjectMergeRequestResourceLabelEventManager` + :attr:`gitlab.v4.objects.ProjectMergeRequest.resourcelabelevents` + :class:`gitlab.v4.objects.GroupEpicResourceLabelEvent` + :class:`gitlab.v4.objects.GroupEpicResourceLabelEventManager` + :attr:`gitlab.v4.objects.GroupEpic.resourcelabelevents` * GitLab API: https://docs.gitlab.com/ee/api/resource_label_events.html Examples -------- Get the events for a resource (issue, merge request or epic):: events = resource.resourcelabelevents.list() Get a specific event for a resource:: event = resource.resourcelabelevents.get(event_id) python-gitlab-2.0.1/docs/gl_objects/messages.rst000066400000000000000000000017341361651701000217040ustar00rootroot00000000000000################## 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` * GitLab API: https://docs.gitlab.com/ce/api/broadcast_messages.html Examples -------- List the messages:: msgs = gl.broadcastmessages.list() Get a single message:: msg = gl.broadcastmessages.get(msg_id) Create a message:: msg = gl.broadcastmessages.create({'message': 'Important information'}) The date format for the ``starts_at`` and ``ends_at`` parameters is ``YYYY-MM-ddThh:mm:ssZ``. Update a message:: msg.font = '#444444' msg.color = '#999999' msg.save() Delete a message:: gl.broadcastmessages.delete(msg_id) # or msg.delete() python-gitlab-2.0.1/docs/gl_objects/milestones.rst000066400000000000000000000031771361651701000222620ustar00rootroot00000000000000########## 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` * 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:: p_milestones = project.milestones.list() g_milestones = group.milestones.list() You can filter the list using the following parameters: * ``iids``: unique IDs of milestones for the project * ``state``: either ``active`` or ``closed`` * ``search``: to search using a string :: p_milestones = project.milestones.list(state='closed') g_milestones = group.milestones.list(state='active') Get a single milestone:: p_milestone = project.milestones.get(milestone_id) g_milestone = group.milestones.get(milestone_id) Create a milestone:: milestone = project.milestones.create({'title': '1.0'}) Edit a milestone:: milestone.description = 'v 1.0 release' milestone.save() Change the state of a milestone (activate / close):: # close a milestone milestone.state_event = 'close' milestone.save() # activate a milestone milestone.state_event = 'activate' milestone.save() List the issues related to a milestone:: issues = milestone.issues() List the merge requests related to a milestone:: merge_requests = milestone.merge_requests() python-gitlab-2.0.1/docs/gl_objects/mr_approvals.rst000066400000000000000000000031121361651701000225720ustar00rootroot00000000000000################################ Merge request approvals settings ################################ Merge request approvals can be defined at the project level or at the merge request level. References ---------- * v4 API: + :class:`gitlab.v4.objects.ProjectApproval` + :class:`gitlab.v4.objects.ProjectApprovalManager` + :class:`gitlab.v4.objects.ProjectApprovalRule` + :class:`gitlab.v4.objects.ProjectApprovalRuleManager` + :attr:`gitlab.v4.objects.Project.approvals` + :class:`gitlab.v4.objects.ProjectMergeRequestApproval` + :class:`gitlab.v4.objects.ProjectMergeRequestApprovalManager` + :attr:`gitlab.v4.objects.ProjectMergeRequest.approvals` * GitLab API: https://docs.gitlab.com/ee/api/merge_request_approvals.html Examples -------- List project-level MR approval rules:: p_mras = project.approvalrules.list() Change project-level MR approval rule:: p_approvalrule.user_ids = [234] p_approvalrule.save() Delete project-level MR approval rule:: p_approvalrule.delete() Get project-level or MR-level MR approvals settings:: p_mras = project.approvals.get() mr_mras = mr.approvals.get() Change project-level or MR-level MR approvals settings:: p_mras.approvals_before_merge = 2 p_mras.save() mr_mras.approvals_before_merge = 2 mr_mras.save() Change project-level or MR-level MR allowed approvers:: project.approvals.set_approvers(approver_ids=[105], approver_group_ids=[653, 654]) mr.approvals.set_approvers(approver_ids=[105], approver_group_ids=[653, 654]) python-gitlab-2.0.1/docs/gl_objects/mrs.rst000066400000000000000000000077251361651701000207040ustar00rootroot00000000000000.. _merge_requests_examples: ############## 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. Merge requests are linked to projects, but they can be listed globally or for groups. Group and global listing ======================== Reference --------- * v4 API: + :class:`gitlab.v4.objects.GroupMergeRequest` + :class:`gitlab.v4.objects.GroupMergeRequestManager` + :attr:`gitlab.v4.objects.Group.mergerequests` + :class:`gitlab.v4.objects.MergeRequest` + :class:`gitlab.v4.objects.MergeRequestManager` + :attr:`gitlab.Gitlab.mergerequests` * GitLab API: https://docs.gitlab.com/ce/api/merge_requests.html Examples -------- List the merge requests available on the GitLab server:: mrs = gl.mergerequests.list() List the merge requests for a group:: group = gl.groups.get('mygroup') mrs = group.mergerequests.list() .. note:: It is not possible to edit or delete ``MergeRequest`` and ``GroupMergeRequest`` objects. You need to create a ``ProjectMergeRequest`` object to apply changes:: mr = group.mergerequests.list()[0] project = gl.projects.get(mr.project_id, lazy=True) editable_mr = project.mergerequests.get(mr.iid, lazy=True) editable_mr.title = updated_title editable_mr.save() Project merge requests ====================== Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectMergeRequest` + :class:`gitlab.v4.objects.ProjectMergeRequestManager` + :attr:`gitlab.v4.objects.Project.mergerequests` * GitLab API: https://docs.gitlab.com/ce/api/merge_requests.html Examples -------- List MRs for a project:: mrs = project.mergerequests.list() You can filter and sort the returned list with the following parameters: * ``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:: mrs = project.mergerequests.list(state='merged', order_by='updated_at') Get a single MR:: mr = project.mergerequests.get(mr_id) Create a MR:: mr = project.mergerequests.create({'source_branch': 'cool_feature', 'target_branch': 'master', 'title': 'merge cool feature', 'labels': ['label1', 'label2']}) Update a MR:: mr.description = 'New description' mr.labels = ['foo', 'bar'] mr.save() Change the state of a MR (close or reopen):: mr.state_event = 'close' # or 'reopen' mr.save() Delete a MR:: project.mergerequests.delete(mr_id) # or mr.delete() Accept a MR:: mr.merge() Cancel a MR when the build succeeds:: mr.cancel_merge_when_pipeline_succeeds() List commits of a MR:: commits = mr.commits() List the changes of a MR:: changes = mr.changes() List the pipelines for a MR:: pipelines = mr.pipelines() List issues that will close on merge:: mr.closes_issues() Subscribe to / unsubscribe from a MR:: mr.subscribe() mr.unsubscribe() Mark a MR as todo:: mr.todo() List the diffs for a merge request:: diffs = mr.diffs.list() Get a diff for a merge request:: diff = mr.diffs.get(diff_id) Get time tracking stats:: merge request.time_stats() On recent versions of Gitlab the time stats are also returned as a merge request object attribute:: mr = project.mergerequests.get(id) print(mr.attributes['time_stats']) Set a time estimate for a merge request:: mr.time_estimate('3h30m') Reset a time estimate for a merge request:: mr.reset_time_estimate() Add spent time for a merge request:: mr.add_spent_time('3h30m') Reset spent time for a merge request:: mr.reset_spent_time() Get user agent detail for the issue (admin only):: detail = issue.user_agent_detail() Attempt to rebase an MR:: mr.rebase() python-gitlab-2.0.1/docs/gl_objects/namespaces.rst000066400000000000000000000006231361651701000222100ustar00rootroot00000000000000########## Namespaces ########## Reference --------- * v4 API: + :class:`gitlab.v4.objects.Namespace` + :class:`gitlab.v4.objects.NamespaceManager` + :attr:`gitlab.Gitlab.namespaces` * GitLab API: https://docs.gitlab.com/ce/api/namespaces.html Examples -------- List namespaces:: namespaces = gl.namespaces.list() Search namespaces:: namespaces = gl.namespaces.list(search='foo') python-gitlab-2.0.1/docs/gl_objects/notes.rst000066400000000000000000000025461361651701000212270ustar00rootroot00000000000000.. _project-notes: ##### Notes ##### You can manipulate notes (comments) on project issues, merge requests and snippets. 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` * GitLab API: https://docs.gitlab.com/ce/api/notes.html Examples -------- List the notes for a resource:: i_notes = issue.notes.list() mr_notes = mr.notes.list() s_notes = snippet.notes.list() Get a note for a resource:: i_note = issue.notes.get(note_id) mr_note = mr.notes.get(note_id) s_note = snippet.notes.get(note_id) Create a note for a resource:: i_note = issue.notes.create({'body': 'note content'}) mr_note = mr.notes.create({'body': 'note content'}) s_note = snippet.notes.create({'body': 'note content'}) Update a note for a resource:: note.body = 'updated note content' note.save() Delete a note for a resource:: note.delete() python-gitlab-2.0.1/docs/gl_objects/notifications.rst000066400000000000000000000034551361651701000227500ustar00rootroot00000000000000##################### 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` * GitLab API: https://docs.gitlab.com/ce/api/notification_settings.html Examples -------- Get the notifications settings:: # 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() Update the notifications settings:: # 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() python-gitlab-2.0.1/docs/gl_objects/pagesdomains.rst000066400000000000000000000023301361651701000225400ustar00rootroot00000000000000############# 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-2.0.1/docs/gl_objects/pipelines_and_jobs.rst000066400000000000000000000177241361651701000237320ustar00rootroot00000000000000################## Pipelines and Jobs ################## 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` * 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) Get variables of a pipeline:: variables = pipeline.variables.list() Create a pipeline for a particular reference with custom variables:: pipeline = project.pipelines.create({'ref': 'master', 'variables': [{'key': 'MY_VARIABLE', 'value': 'hello'}]}) Retry the failed builds for a pipeline:: pipeline.retry() Cancel builds in a pipeline:: pipeline.cancel() Delete a pipeline:: pipeline.delete() 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` * GitLab API: https://docs.gitlab.com/ce/api/pipeline_triggers.html Examples -------- List triggers:: triggers = project.triggers.list() Get a trigger:: trigger = project.triggers.get(trigger_token) Create a trigger:: trigger = project.triggers.create({'description': 'mytrigger'}) Remove a trigger:: project.triggers.delete(trigger_token) # or trigger.delete() Full example with wait for finish:: def get_or_create_trigger(project): trigger_decription = 'my_trigger_id' for t in project.triggers.list(): if t.description == trigger_decription: return t return project.triggers.create({'description': trigger_decription}) trigger = get_or_create_trigger(project) pipeline = project.trigger_pipeline('master', trigger.token, variables={"DEPLOY_ZONE": "us-west1"}) while pipeline.finished_at is None: pipeline.refresh() time.sleep(1) You can trigger a pipeline using token authentication instead of user authentication. To do so create an anonymous Gitlab instance and use lazy objects to get the associated project:: gl = gitlab.Gitlab(URL) # no authentication project = gl.projects.get(project_id, lazy=True) # no API call project.trigger_pipeline('master', trigger_token) Reference: https://docs.gitlab.com/ee/ci/triggers/#trigger-token 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() List schedule variables:: # note: you need to use get() to retrieve the schedule variables. The # attribute is not present in the response of a list() call sched = projects.pipelineschedules.get(schedule_id) vars = sched.attributes['variables'] 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` * 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:: p_variables = project.variables.list() g_variables = group.variables.list() Get a variable:: p_var = project.variables.get('key_name') g_var = group.variables.get('key_name') Create a variable:: var = project.variables.create({'key': 'key1', 'value': 'value1'}) var = group.variables.create({'key': 'key1', 'value': 'value1'}) Update a variable value:: var.value = 'new_value' var.save() Remove a variable:: project.variables.delete('key_name') group.variables.delete('key_name') # or var.delete() Jobs ==== Jobs are associated to projects, pipelines and commits. They provide information on the 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` * GitLab API: https://docs.gitlab.com/ce/api/jobs.html Examples -------- Jobs are usually automatically triggered, but you can explicitly trigger a new job:: project.trigger_build('master', trigger_token, {'extra_var1': 'foo', 'extra_var2': 'bar'}) List jobs for the project:: jobs = project.jobs.list() Get a single job:: project.jobs.get(job_id) List the jobs of a pipeline:: project = gl.projects.get(project_id) pipeline = project.pipelines.get(pipeline_id) jobs = pipeline.jobs.list() .. note:: Job methods (play, cancel, and so on) are not available on ``ProjectPipelineJob`` objects. To use these methods create a ``ProjectJob`` object:: pipeline_job = pipeline.jobs.list()[0] job = project.jobs.get(pipeline_job.id, lazy=True) job.retry() Get the artifacts of a job:: build_or_job.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:: 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 You can also directly stream the output into a file, and unzip it afterwards:: 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) Get a single artifact file:: build_or_job.artifact('path/to/file') Get a single artifact file by branch and job:: project.artifact('branch', 'path/to/file', 'job') Mark a job artifact as kept when expiration is set:: build_or_job.keep_artifacts() Delete the artifacts of a job:: build_or_job.delete_artifacts() Get a job trace:: build_or_job.trace() .. warning:: Traces are entirely stored in memory unless you use the streaming feature. See :ref:`the artifacts example `. Cancel/retry a job:: build_or_job.cancel() build_or_job.retry() Play (trigger) a job:: build_or_job.play() Erase a job (artifacts and trace):: build_or_job.erase() python-gitlab-2.0.1/docs/gl_objects/projects.rst000066400000000000000000000450451361651701000217310ustar00rootroot00000000000000######## Projects ######## Projects ======== Reference --------- * v4 API: + :class:`gitlab.v4.objects.Project` + :class:`gitlab.v4.objects.ProjectManager` + :attr:`gitlab.Gitlab.projects` * GitLab API: https://docs.gitlab.com/ce/api/projects.html Examples -------- List projects:: projects = gl.projects.list() 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``) :: # List all projects (default 20) projects = gl.projects.list(all=True) # 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.list(owned=True) # List starred projects projects = gl.projects.list(starred=True) # Search projects projects = gl.projects.list(search='keyword') .. note:: Fetching a list of projects, doesn't include all attributes of all projects. To retrieve all attributes, you'll need to fetch a single project Get a single project:: # Get a project by ID project_id = 851 project = gl.projects.get(project_id) Create a project:: project = gl.projects.create({'name': 'project1'}) Create a project for a user (admin only):: alice = gl.users.list(username='alice')[0] user_project = alice.projects.create({'name': 'project'}) user_projects = alice.projects.list() 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 group_id = gl.groups.list(search='my-group')[0].id project = gl.projects.create({'name': 'myrepo', 'namespace_id': group_id}) Update a project:: project.snippets_enabled = 1 project.save() Set the avatar image for a project:: # the avatar image can be passed as data (content of the file) or as a file # object opened in binary mode project.avatar = open('path/to/file.png', 'rb') project.save() Delete a project:: gl.projects.delete(project_id) # or project.delete() Fork a project:: fork = project.forks.create({}) # fork to a specific namespace fork = project.forks.create({'namespace': 'myteam'}) Get a list of forks for the project:: forks = project.forks.list() Create/delete a fork relation between projects (requires admin permissions):: project.create_fork_relation(source_project.id) project.delete_fork_relation() Get languages used in the project with percentage value:: languages = project.languages() Star/unstar a project:: project.star() project.unstar() Archive/unarchive a project:: project.archive() project.unarchive() Start the housekeeping job:: project.housekeeping() List the 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') Get the content and metadata of a file for a commit, using a blob sha:: 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'] Update a project submodule:: items = project.update_submodule( submodule="foo/bar", branch="master", commit_sha="4c3674f66071e30b3311dac9b9ccc90502a72664", commit_message="Message", # optional ) Get the repository archive:: tgz = project.repository_archive() # get the archive for a branch/tag/commit tgz = project.repository_archive(sha='4567abc') .. 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:: # 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) .. warning:: Blobs are entirely stored in memory unless you use the streaming feature. See :ref:`the artifacts example `. Get a snapshot of the repository:: tar_file = project.snapshot() .. warning:: Snapshots are entirely stored in memory unless you use the streaming feature. See :ref:`the artifacts example `. Compare two branches, tags or commits:: 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) Get a list of contributors for the repository:: contributors = project.repository_contributors() Get a list of users for the repository:: users = p.users.list() # search for users users = p.users.list(search='pattern') Start the pull mirroring process (EE edition):: project.mirror_pull() Import / Export =============== You can export projects from gitlab, and re-import them to create new projects or overwrite existing ones. Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectExport` + :class:`gitlab.v4.objects.ProjectExportManager` + :attr:`gitlab.v4.objects.Project.exports` + :class:`gitlab.v4.objects.ProjectImport` + :class:`gitlab.v4.objects.ProjectImportManager` + :attr:`gitlab.v4.objects.Project.imports` + :attr:`gitlab.v4.objects.ProjectManager.import_project` * GitLab API: https://docs.gitlab.com/ce/api/project_import_export.html Examples -------- A project export is an asynchronous operation. To retrieve the archive generated by GitLab you need to: #. Create an export using the API #. Wait for the export to be done #. Download the result :: # Create the export p = gl.projects.get(my_project) export = p.exports.create({}) # Wait for the 'finished' status export.refresh() while export.export_status != 'finished': time.sleep(1) export.refresh() # Download the result with open('/tmp/export.tgz', 'wb') as f: export.download(streamed=True, action=f.write) Import the project:: output = gl.projects.import_project(open('/tmp/export.tgz', 'rb'), 'my_new_project') # Get a ProjectImport object to track the import status project_import = gl.projects.get(output['id'], lazy=True).imports.get() while project_import.import_status != 'finished': time.sleep(1) project_import.refresh() 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` * GitLab API: https://docs.gitlab.com/ce/api/repository_files.html Examples -------- Get a file:: 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()) Get a raw file:: raw_content = project.files.raw(file_path='README.rst', ref='master') print(raw_content) with open('/tmp/raw-download.txt', 'wb') as f: project.files.raw(file_path='README.rst', ref='master', streamed=True, action=f.write) Create a new file:: f = project.files.create({'file_path': 'testfile.txt', 'branch': 'master', 'content': file_content, 'author_email': 'test@example.com', 'author_name': 'yourname', 'encoding': 'text', 'commit_message': 'Create testfile'}) Update a file. The entire content must be uploaded, as plain text or as base64 encoded text:: f.content = 'new content' f.save(branch='master', commit_message='Update testfile') # 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') Delete a file:: f.delete(commit_message='Delete testfile', branch='master') Get file blame:: b = project.files.blame(file_path='README.rst', ref='master') Project tags ============ Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectTag` + :class:`gitlab.v4.objects.ProjectTagManager` + :attr:`gitlab.v4.objects.Project.tags` * GitLab API: https://docs.gitlab.com/ce/api/tags.html Examples -------- List the project tags:: tags = project.tags.list() Get a tag:: tag = project.tags.get('1.0') Create a tag:: tag = project.tags.create({'tag_name': '1.0', 'ref': 'master'}) Set or update the release note for a tag:: tag.set_release_description('awesome v1.0 release') Delete a tag:: project.tags.delete('1.0') # or tag.delete() .. _project_snippets: Project snippets ================ The snippet visibility can be defined 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` * GitLab API: https://docs.gitlab.com/ce/api/project_snippets.html Examples -------- List the project snippets:: snippets = project.snippets.list() Get a snippet:: snippet = project.snippets.get(snippet_id) Get the content of a snippet:: print(snippet.content()) .. warning:: The snippet content is entirely stored in memory unless you use the streaming feature. See :ref:`the artifacts example `. Create a snippet:: snippet = project.snippets.create({'title': 'sample 1', 'file_name': 'foo.py', 'code': 'import gitlab', 'visibility_level': gitlab.VISIBILITY_PRIVATE}) Update a snippet:: snippet.code = 'import gitlab\nimport whatever' snippet.save Delete a snippet:: project.snippets.delete(snippet_id) # or snippet.delete() Get user agent detail (admin only):: detail = snippet.user_agent_detail() Notes ===== See :ref:`project-notes`. Project members =============== Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectMember` + :class:`gitlab.v4.objects.ProjectMemberManager` + :attr:`gitlab.v4.objects.Project.members` * GitLab API: https://docs.gitlab.com/ce/api/members.html Examples -------- List the project members:: members = project.members.list() List the project members recursively (including inherited members through ancestor groups):: members = project.members.all(all=True) Search project members matching a query string:: members = project.members.list(query='bar') Get a single project member:: member = project.members.get(user_id) Add a project member:: member = project.members.create({'user_id': user.id, 'access_level': gitlab.DEVELOPER_ACCESS}) Modify a project member (change the access level):: member.access_level = gitlab.MAINTAINER_ACCESS member.save() Remove a member from the project team:: project.members.delete(user.id) # or member.delete() Share/unshare the project with a group:: project.share(group.id, gitlab.DEVELOPER_ACCESS) project.unshare(group.id) Project hooks ============= Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectHook` + :class:`gitlab.v4.objects.ProjectHookManager` + :attr:`gitlab.v4.objects.Project.hooks` * GitLab API: https://docs.gitlab.com/ce/api/projects.html#hooks Examples -------- List the project hooks:: hooks = project.hooks.list() Get a project hook:: hook = project.hooks.get(hook_id) Create a project hook:: hook = project.hooks.create({'url': 'http://my/action/url', 'push_events': 1}) Update a project hook:: hook.push_events = 0 hook.save() Delete a project hook:: project.hooks.delete(hook_id) # or hook.delete() Project Services ================ Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectService` + :class:`gitlab.v4.objects.ProjectServiceManager` + :attr:`gitlab.v4.objects.Project.services` * GitLab API: https://docs.gitlab.com/ce/api/services.html Examples --------- Get a service:: service = project.services.get('asana') # display its status (enabled/disabled) print(service.active) List the code names of available services (doesn't return objects):: services = project.services.available() Configure and enable a service:: service.api_key = 'randomkey' service.save() Disable a service:: service.delete() File uploads ============ Reference --------- * v4 API: + :attr:`gitlab.v4.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:: project.upload("filename.txt", filepath="/some/path/filename.txt") Upload a file into a project without a filesystem path:: project.upload("filename.txt", filedata="Raw data") Upload a file and comment on an issue using the uploaded file's 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"]) }) Upload a file and comment on an issue while using custom markdown to reference the uploaded file:: 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"]) }) Project push rules ================== Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectPushRules` + :class:`gitlab.v4.objects.ProjectPushRulesManager` + :attr:`gitlab.v4.objects.Project.pushrules` * GitLab API: https://docs.gitlab.com/ee/api/projects.html#push-rules Examples --------- Create project push rules (at least one rule is necessary):: project.pushrules.create({'deny_delete_tag': True}) Get project push rules (returns None is there are no push rules):: pr = project.pushrules.get() Edit project push rules:: pr.branch_name_regex = '^(master|develop|support-\d+|release-\d+\..+|hotfix-.+|feature-.+)$' pr.save() Delete project push rules:: pr.delete() Project releases ================ Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectRelease` + :class:`gitlab.v4.objects.ProjectReleaseManager` + :attr:`gitlab.v4.objects.Project.releases` * Gitlab API: https://docs.gitlab.com/ee/api/releases/index.html Examples -------- Get a list of releases from a project:: release = project.releases.list() Get a single release:: release = project.releases.get('v1.2.3') Create a release for a project tag:: release = project.releases.create({'name':'Demo Release', 'tag_name':'v1.2.3', 'description':'release notes go here'}) Delete a release:: release = p.releases.delete('v1.2.3') Project protected tags ====================== Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectProtectedTag` + :class:`gitlab.v4.objects.ProjectProtectedTagManager` + :attr:`gitlab.v4.objects.Project.protectedtags` * GitLab API: https://docs.gitlab.com/ce/api/protected_tags.html Examples --------- Get a list of protected tags from a project:: protected_tags = project.protectedtags.list() Get a single protected tag or wildcard protected tag:: protected_tag = project.protectedtags.get('v*') Protect a single repository tag or several project repository tags using a wildcard protected tag:: project.protectedtags.create({'name': 'v*', 'create_access_level': '40'}) Unprotect the given protected tag or wildcard protected tag.:: protected_tag.delete() Additional project statistics ============================= Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectAdditionalStatistics` + :class:`gitlab.v4.objects.ProjectAdditionalStatisticsManager` + :attr:`gitlab.v4.objects.Project.additionalstatistics` * GitLab API: https://docs.gitlab.com/ce/api/project_statistics.html Examples --------- Get all additional statistics of a project:: statistics = project.additionalstatistics.get() Get total fetches in last 30 days of a project:: total_fetches = project.additionalstatistics.get()['fetches']['total'] Project issues statistics ========================= Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectIssuesStatistics` + :class:`gitlab.v4.objects.ProjectIssuesStatisticsManager` + :attr:`gitlab.v4.objects.Project.issuesstatistics` * GitLab API: https://docs.gitlab.com/ce/api/issues_statistics.html#get-project-issues-statistics Examples --------- Get statistics of all issues in a project:: statistics = project.issuesstatistics.get() Get statistics of issues in a project with ``foobar`` in ``title`` and ``description``:: statistics = project.issuesstatistics.get(search='foobar') python-gitlab-2.0.1/docs/gl_objects/protected_branches.rst000066400000000000000000000024721361651701000237330ustar00rootroot00000000000000################## 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:: p_branches = project.protectedbranches.list() Get a single protected branch:: p_branch = project.protectedbranches.get('master') Create a protected branch:: p_branch = project.protectedbranches.create({ 'name': '*-stable', 'merge_access_level': gitlab.DEVELOPER_ACCESS, 'push_access_level': gitlab.MAINTAINER_ACCESS }) Create a protected branch with more granular access control:: p_branch = project.protectedbranches.create({ 'name': '*-stable', 'allowed_to_push': [{"user_id": 99}, {"user_id": 98}], 'allowed_to_merge': [{"group_id": 653}], 'allowed_to_unprotect': [{"access_level": gitlab.MAINTAINER_ACCESS}] }) Delete a protected branch:: project.protectedbranches.delete('*-stable') # or p_branch.delete() python-gitlab-2.0.1/docs/gl_objects/repositories.rst000066400000000000000000000011651361651701000226220ustar00rootroot00000000000000##################### Registry Repositories ##################### References ---------- * v4 API: + :class:`gitlab.v4.objects.ProjectRegistryRepository` + :class:`gitlab.v4.objects.ProjectRegistryRepositoryManager` + :attr:`gitlab.v4.objects.Project.repositories` * Gitlab API: https://docs.gitlab.com/ce/api/container_registry.html Examples -------- Get the list of container registry repositories associated with the project:: repositories = project.repositories.list() Delete repository:: project.repositories.delete(id=x) # or repository = repositories.pop() repository.delete() python-gitlab-2.0.1/docs/gl_objects/repository_tags.rst000066400000000000000000000021501361651701000233230ustar00rootroot00000000000000######################## Registry Repository Tags ######################## References ---------- * v4 API: + :class:`gitlab.v4.objects.ProjectRegistryTag` + :class:`gitlab.v4.objects.ProjectRegistryTagManager` + :attr:`gitlab.v4.objects.Repository.tags` * Gitlab API: https://docs.gitlab.com/ce/api/container_registry.html Examples -------- Get the list of repository tags in given registry:: repositories = project.repositories.list() repository = repositories.pop() tags = repository.tags.list() Get specific tag:: repository.tags.get(id=tag_name) Delete tag:: repository.tags.delete(id=tag_name) # or tag = repository.tags.get(id=tag_name) tag.delete() Delete tag in bulk:: repository.tags.delete_in_bulk(keep_n=1) # or repository.tags.delete_in_bulk(older_than="1m") # or repository.tags.delete_in_bulk(name_regex="v.+", keep_n=2) .. note:: Delete in bulk is asynchronous operation and may take a while. Refer to: https://docs.gitlab.com/ce/api/container_registry.html#delete-repository-tags-in-bulk python-gitlab-2.0.1/docs/gl_objects/runners.rst000066400000000000000000000052211361651701000215640ustar00rootroot00000000000000####### 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` * 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. :: # 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') Get a runner's detail:: runner = gl.runners.get(runner_id) Register a new runner:: runner = gl.runners.create({'token': secret_token}) Update a runner:: runner = gl.runners.get(runner_id) runner.tag_list.append('new_tag') runner.save() Remove a runner:: gl.runners.delete(runner_id) # or runner.delete() Verify a registered runner token:: try: gl.runners.verify(runner_token) print("Valid token") except GitlabVerifyError: print("Invalid token") Project runners =============== Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectRunner` + :class:`gitlab.v4.objects.ProjectRunnerManager` + :attr:`gitlab.v4.objects.Project.runners` * GitLab API: https://docs.gitlab.com/ce/api/runners.html Examples -------- List the runners for a project:: runners = project.runners.list() Enable a specific runner for a project:: p_runner = project.runners.create({'runner_id': runner.id}) Disable a specific runner for a project:: project.runners.delete(runner.id) Runner jobs =========== Reference --------- * v4 API: + :class:`gitlab.v4.objects.RunnerJob` + :class:`gitlab.v4.objects.RunnerJobManager` + :attr:`gitlab.v4.objects.Runner.jobs` * GitLab API: https://docs.gitlab.com/ce/api/runners.html Examples -------- List for jobs for a runner:: jobs = runner.jobs.list() Filter the list using the jobs status:: # status can be 'running', 'success', 'failed' or 'canceled' active_jobs = runner.jobs.list(status='running') python-gitlab-2.0.1/docs/gl_objects/search.rst000066400000000000000000000026541361651701000213440ustar00rootroot00000000000000########## Search API ########## You can search for resources at the top level, in a project or in a group. Searches are based on a scope (issues, merge requests, and so on) and a search string. Reference --------- * v4 API: + :attr:`gitlab.Gitlab.search` + :attr:`gitlab.v4.objects.Group.search` + :attr:`gitlab.v4.objects.Project.search` * GitLab API: https://docs.gitlab.com/ce/api/search.html Examples -------- Search for issues matching a specific string:: # global search gl.search('issues', 'regression') # group search group = gl.groups.get('mygroup') group.search('issues', 'regression') # project search project = gl.projects.get('myproject') project.search('issues', 'regression') The ``search()`` methods implement the pagination support:: # get lists of 10 items, and start at page 2 gl.search('issues', search_str, page=2, per_page=10) # get a generator that will automatically make required API calls for # pagination for item in gl.search('issues', search_str, as_list=False): do_something(item) The search API doesn't return objects, but dicts. If you need to act on objects, you need to create them explicitly:: for item in gl.search('issues', search_str, as_list=False): issue_project = gl.projects.get(item['project_id'], lazy=True) issue = issue_project.issues.get(item['iid']) issue.state = 'closed' issue.save() python-gitlab-2.0.1/docs/gl_objects/settings.rst000066400000000000000000000006411361651701000217310ustar00rootroot00000000000000######## Settings ######## Reference --------- * v4 API: + :class:`gitlab.v4.objects.ApplicationSettings` + :class:`gitlab.v4.objects.ApplicationSettingsManager` + :attr:`gitlab.Gitlab.settings` * GitLab API: https://docs.gitlab.com/ce/api/settings.html Examples -------- Get the settings:: settings = gl.settings.get() Update the settings:: settings.signin_enabled = False settings.save() python-gitlab-2.0.1/docs/gl_objects/sidekiq.rst000066400000000000000000000006111361651701000215170ustar00rootroot00000000000000############### Sidekiq metrics ############### Reference --------- * v4 API: + :class:`gitlab.v4.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-2.0.1/docs/gl_objects/snippets.rst000066400000000000000000000026751361651701000217470ustar00rootroot00000000000000######## Snippets ######## Reference ========= * v4 API: + :class:`gitlab.v4.objects.Snippet` + :class:`gitlab.v4.objects.SnipptManager` + :attr:`gitlab.Gitlab.snippets` * GitLab API: https://docs.gitlab.com/ce/api/snippets.html Examples ======== List snippets owned by the current user:: snippets = gl.snippets.list() List the public snippets:: public_snippets = gl.snippets.public() Get a snippet:: snippet = gl.snippets.get(snippet_id) # get the content content = snippet.content() .. warning:: Blobs are entirely stored in memory unless you use the streaming feature. See :ref:`the artifacts example `. Create a snippet:: snippet = gl.snippets.create({'title': 'snippet1', 'file_name': 'snippet1.py', 'content': open('snippet1.py').read()}) Update the snippet attributes:: snippet.visibility_level = gitlab.VISIBILITY_PUBLIC snippet.save() To update a snippet code you need to create a ``ProjectSnippet`` object:: snippet = gl.snippets.get(snippet_id) project = gl.projects.get(snippet.projec_id, lazy=True) editable_snippet = project.snippets.get(snippet.id) editable_snippet.code = new_snippet_content editable_snippet.save() Delete a snippet:: gl.snippets.delete(snippet_id) # or snippet.delete() Get user agent detail (admin only):: detail = snippet.user_agent_detail() python-gitlab-2.0.1/docs/gl_objects/system_hooks.rst000066400000000000000000000011241361651701000226150ustar00rootroot00000000000000############ System hooks ############ Reference --------- * v4 API: + :class:`gitlab.v4.objects.Hook` + :class:`gitlab.v4.objects.HookManager` + :attr:`gitlab.Gitlab.hooks` * GitLab API: https://docs.gitlab.com/ce/api/system_hooks.html Examples -------- List the system hooks:: hooks = gl.hooks.list() Create a system hook:: gl.hooks.get(hook_id) Test a system hook. The returned object is not usable (it misses the hook ID):: hook = gl.hooks.create({'url': 'http://your.target.url'}) Delete a system hook:: gl.hooks.delete(hook_id) # or hook.delete() python-gitlab-2.0.1/docs/gl_objects/templates.rst000066400000000000000000000040161361651701000220670ustar00rootroot00000000000000######### 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` * GitLab API: https://docs.gitlab.com/ce/api/templates/licenses.html Examples -------- List known license templates:: licenses = gl.licenses.list() Generate a license content for a project:: license = gl.licenses.get('apache-2.0', project='foobar', fullname='John Doe') print(license.content) .gitignore templates ==================== Reference --------- * v4 API: + :class:`gitlab.v4.objects.Gitignore` + :class:`gitlab.v4.objects.GitignoreManager` + :attr:`gitlab.Gitlab.gitignores` * GitLab API: https://docs.gitlab.com/ce/api/templates/gitignores.html Examples -------- List known gitignore templates:: gitignores = gl.gitignores.list() Get a gitignore template:: gitignore = gl.gitignores.get('Python') print(gitignore.content) GitLab CI templates =================== Reference --------- * v4 API: + :class:`gitlab.v4.objects.Gitlabciyml` + :class:`gitlab.v4.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:: gitlabciymls = gl.gitlabciymls.list() Get a GitLab CI template:: gitlabciyml = gl.gitlabciymls.get('Pelican') print(gitlabciyml.content) 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:: dockerfiles = gl.dockerfiles.list() Get a Dockerfile template:: dockerfile = gl.dockerfiles.get('Python') print(dockerfile.content) python-gitlab-2.0.1/docs/gl_objects/todos.rst000066400000000000000000000015101361651701000212150ustar00rootroot00000000000000##### Todos ##### Reference --------- * v4 API: + :class:`~gitlab.objects.Todo` + :class:`~gitlab.objects.TodoManager` + :attr:`gitlab.Gitlab.todos` * GitLab API: https://docs.gitlab.com/ce/api/todos.html Examples -------- List active todos:: todos = gl.todos.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:: todos = gl.todos.list(project_id=1) todos = gl.todos.list(state='done', type='Issue') Mark a todo as done:: todos = gl.todos.list(project_id=1) todos[0].mark_as_done() Mark all the todos as done:: gl.todos.mark_all_as_done() python-gitlab-2.0.1/docs/gl_objects/users.rst000066400000000000000000000164321361651701000212370ustar00rootroot00000000000000###################### 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` * GitLab API: https://docs.gitlab.com/ce/api/users.html Examples -------- Get the list of users:: users = gl.users.list() Search users whose username match a given string:: users = gl.users.list(search='foo') Get a single user:: # by ID user = gl.users.get(user_id) # by username user = gl.users.list(username='root')[0] Create a user:: user = gl.users.create({'email': 'john@doe.com', 'password': 's3cur3s3cr3T', 'username': 'jdoe', 'name': 'John Doe'}) Update a user:: user.name = 'Real Name' user.save() Delete a user:: gl.users.delete(user_id) # or user.delete() Block/Unblock a user:: user.block() user.unblock() Activate/Deactivate a user:: user.activate() user.deactivate() Set the avatar image for a user:: # the avatar image can be passed as data (content of the file) or as a file # object opened in binary mode user.avatar = open('path/to/file.png', 'rb') user.save() Set an external identity for a user:: user.provider = 'oauth2_generic' user.extern_uid = '3' user.save() 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:: i_t = user.impersonationtokens.list(state='active') i_t = user.impersonationtokens.list(state='inactive') Get an impersonation token for a user:: i_t = user.impersonationtokens.get(i_t_id) Create and use an impersonation token for a user:: 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) Revoke (delete) an impersonation token for a user:: i_t.delete() Current User ============ References ---------- * v4 API: + :class:`gitlab.v4.objects.CurrentUser` + :class:`gitlab.v4.objects.CurrentUserManager` + :attr:`gitlab.Gitlab.user` * GitLab API: https://docs.gitlab.com/ce/api/users.html Examples -------- Get the current user:: gl.auth() current_user = gl.user 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 Examples -------- List GPG keys for a user:: gpgkeys = user.gpgkeys.list() Get a GPG gpgkey for a user:: gpgkey = user.gpgkeys.get(key_id) Create a GPG gpgkey for a user:: # get the key with `gpg --export -a GPG_KEY_ID` k = user.gpgkeys.create({'key': public_key_content}) Delete a GPG gpgkey for a user:: user.gpgkeys.delete(key_id) # or 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` * GitLab API: https://docs.gitlab.com/ce/api/users.html#list-ssh-keys Examples -------- List SSH keys for a user:: keys = user.keys.list() Create an SSH key for a user:: k = user.keys.create({'title': 'my_key', 'key': open('/home/me/.ssh/id_rsa.pub').read()}) Delete an SSH key for a user:: user.keys.delete(key_id) # or key.delete() Status ====== References ---------- You can manipulate the status for the current user and you can read the status of other users. * v4 API: + :class:`gitlab.v4.objects.CurrentUserStatus` + :class:`gitlab.v4.objects.CurrentUserStatusManager` + :attr:`gitlab.v4.objects.CurrentUser.status` + :class:`gitlab.v4.objects.UserStatus` + :class:`gitlab.v4.objects.UserStatusManager` + :attr:`gitlab.v4.objects.User.status` * GitLab API: https://docs.gitlab.com/ce/api/users.html#user-status Examples -------- Get current user status:: status = user.status.get() Update the status for the current user:: status = user.status.get() status.message = "message" status.emoji = "thumbsup" status.save() Get the status of other users:: gl.users.get(1).status.get() 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` * GitLab API: https://docs.gitlab.com/ce/api/users.html#list-emails Examples -------- List emails for a user:: emails = user.emails.list() Get an email for a user:: email = user.emails.get(email_id) Create an email for a user:: k = user.emails.create({'email': 'foo@bar.com'}) Delete an email for a user:: user.emails.delete(email_id) # or email.delete() Users activities ================ References ---------- * 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:: activities = gl.user_activities.list( query_parameters={'from': '2018-07-01'}, all=True, as_list=False) python-gitlab-2.0.1/docs/gl_objects/wikis.rst000066400000000000000000000012611361651701000212160ustar00rootroot00000000000000########## Wiki pages ########## References ========== * v4 API: + :class:`gitlab.v4.objects.ProjectWiki` + :class:`gitlab.v4.objects.ProjectWikiManager` + :attr:`gitlab.v4.objects.Project.wikis` * GitLab API: https://docs.gitlab.com/ce/api/wikis.html Examples -------- Get the list of wiki pages for a project:: pages = project.wikis.list() Get a single wiki page:: page = project.wikis.get(page_slug) Create a wiki page:: page = project.wikis.create({'title': 'Wiki Page 1', 'content': open(a_file).read()}) Update a wiki page:: page.content = 'My new content' page.save() Delete a wiki page:: page.delete() python-gitlab-2.0.1/docs/index.rst000066400000000000000000000010571361651701000170670ustar00rootroot00000000000000.. 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 faq switching-to-v4 api-objects api/gitlab release_notes changelog Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` python-gitlab-2.0.1/docs/install.rst000066400000000000000000000010701361651701000174210ustar00rootroot00000000000000############ 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-2.0.1/docs/make.bat000066400000000000000000000150731361651701000166360ustar00rootroot00000000000000@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-2.0.1/docs/release_notes.rst000066400000000000000000000000421361651701000206010ustar00rootroot00000000000000.. include:: ../RELEASE_NOTES.rst python-gitlab-2.0.1/docs/switching-to-v4.rst000066400000000000000000000062731361651701000207330ustar00rootroot00000000000000.. _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 does not support the v3 API anymore, and you should consider switching to v4 if you use a recent version of GitLab (>= 9.0), or if you use https://gitlab.com. Using the v4 API ================ python-gitlab uses the v4 API by default since the 1.3.0 release. If you are migrating from an older release, make sure that you remove the ``api_version`` definition in you constructors and configuration file: The following examples are **not valid** anymore: .. code-block:: python gl = gitlab.Gitlab(..., api_version=3) .. 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-2.0.1/gitlab/000077500000000000000000000000001361651701000155355ustar00rootroot00000000000000python-gitlab-2.0.1/gitlab/__init__.py000066400000000000000000000730131361651701000176520ustar00rootroot00000000000000# -*- 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 time import warnings import requests import gitlab.config from gitlab.const import * # noqa from gitlab.exceptions import * # noqa from gitlab import utils # noqa __title__ = "python-gitlab" __version__ = "2.0.1" __author__ = "Gauvain Pocentek" __email__ = "gauvainpocentek@gmail.com" __license__ = "LGPL3" __copyright__ = "Copyright 2013-2019 Gauvain Pocentek" warnings.filterwarnings("default", category=DeprecationWarning, module="^gitlab") REDIRECT_MSG = ( "python-gitlab detected an http to https redirection. You " "must update your GitLab URL to use https:// to avoid issues." ) def _sanitize(value): if isinstance(value, dict): return dict((k, _sanitize(v)) for k, v in value.items()) if isinstance(value, str): 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 job_token (str): A CI job token 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 (support for 4 only) pagination (str): Can be set to 'keyset' to use keyset pagination order_by (str): Set order_by globally """ def __init__( self, url, private_token=None, oauth_token=None, job_token=None, ssl_verify=True, http_username=None, http_password=None, timeout=None, api_version="4", session=None, per_page=None, pagination=None, order_by=None, ): self._api_version = str(api_version) self._server_version = self._server_revision = None self._base_url = url 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 = {"User-Agent": "%s/%s" % (__title__, __version__)} #: 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.job_token = job_token self._set_auth_info() #: Create a session object for requests self.session = session or requests.Session() self.per_page = per_page self.pagination = pagination self.order_by = order_by 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.geonodes = objects.GeoNodeManager(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.ldapgroups = objects.LDAPGroupManager(self) self.licenses = objects.LicenseManager(self) self.namespaces = objects.NamespaceManager(self) self.mergerequests = objects.MergeRequestManager(self) self.notificationsettings = objects.NotificationSettingsManager(self) self.projects = objects.ProjectManager(self) self.runners = objects.RunnerManager(self) self.settings = objects.ApplicationSettingsManager(self) self.appearance = objects.ApplicationAppearanceManager(self) self.sidekiq = objects.SidekiqManager(self) self.snippets = objects.SnippetManager(self) self.users = objects.UserManager(self) self.todos = objects.TodoManager(self) self.dockerfiles = objects.DockerfileManager(self) self.events = objects.EventManager(self) self.audit_events = objects.AuditEventManager(self) self.features = objects.FeatureManager(self) self.pagesdomains = objects.PagesDomainManager(self) self.user_activities = objects.UserActivitiesManager(self) 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 url(self): """The user-provided server URL.""" return self._base_url @property def api_url(self): """The computed API base URL.""" return self._url @property def api_version(self): """The API version used (4 only).""" return self._api_version @classmethod def from_config(cls, 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 cls( config.url, private_token=config.private_token, oauth_token=config.oauth_token, job_token=config.job_token, ssl_verify=config.ssl_verify, timeout=config.timeout, http_username=config.http_username, http_password=config.http_password, api_version=config.api_version, per_page=config.per_page, pagination=config.pagination, order_by=config.order_by, ) def auth(self): """Performs an authentication using private token. The `user` attribute will hold a `gitlab.objects.CurrentUser` object on success. """ 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. ('unknown', 'unknwown') if the server doesn't perform as expected. """ if self._server_version is None: try: data = self.http_get("/version") self._server_version = data["version"] self._server_revision = data["revision"] except Exception: self._server_version = self._server_revision = "unknown" return self._server_version, self._server_revision @on_http_error(GitlabVerifyError) def lint(self, content, **kwargs): """Validate a gitlab CI configuration. Args: content (txt): The .gitlab-ci.yml content **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabVerifyError: If the validation could not be done Returns: tuple: (True, []) if the file is valid, (False, errors(list)) otherwise """ post_data = {"content": content} data = self.http_post("/ci/lint", post_data=post_data, **kwargs) return (data["status"] == "valid", data["errors"]) @on_http_error(GitlabMarkdownError) def markdown(self, text, gfm=False, project=None, **kwargs): """Render an arbitrary Markdown document. Args: text (str): The markdown text to render gfm (bool): Render text using GitLab Flavored Markdown. Default is False project (str): Full path of a project used a context when `gfm` is True **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabMarkdownError: If the server cannot perform the request Returns: str: The HTML rendering of the markdown text. """ post_data = {"text": text, "gfm": gfm} if project is not None: post_data["project"] = project data = self.http_post("/markdown", post_data=post_data, **kwargs) return data["html"] @on_http_error(GitlabLicenseError) def get_license(self, **kwargs): """Retrieve information about the current license. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabGetError: If the server cannot perform the request Returns: dict: The current license information """ return self.http_get("/license", **kwargs) @on_http_error(GitlabLicenseError) def set_license(self, license, **kwargs): """Add a new license. Args: license (str): The license string **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabPostError: If the server cannot perform the request Returns: dict: The new license information """ data = {"license": license} return self.http_post("/license", post_data=data, **kwargs) 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) url = obj_url % args if id_ is not None: return "%s/%s" % (url, str(id_)) else: return url def _set_auth_info(self): tokens = [ token for token in [self.private_token, self.oauth_token, self.job_token] if token ] if len(tokens) > 1: raise ValueError( "Only one of private_token, oauth_token or job_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.pop("Authorization", None) self.headers["PRIVATE-TOKEN"] = self.private_token self.headers.pop("JOB-TOKEN", None) if self.oauth_token: self.headers["Authorization"] = "Bearer %s" % self.oauth_token self.headers.pop("PRIVATE-TOKEN", None) self.headers.pop("JOB-TOKEN", None) if self.job_token: self.headers.pop("Authorization", None) self.headers.pop("PRIVATE-TOKEN", None) self.headers["JOB-TOKEN"] = self.job_token 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 # noqa 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 _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. Returns: str: The full URL """ if path.startswith("http://") or path.startswith("https://"): return path else: return "%s%s" % (self._url, path) def _check_redirects(self, result): # Check the requests history to detect http to https redirections. # If the initial verb is POST, the next request will use a GET request, # leading to an unwanted behaviour. # If the initial verb is PUT, the data will not be send with the next # request. # If we detect a redirection to https with a POST or a PUT request, we # raise an exception with a useful error message. if result.history and self._base_url.startswith("http:"): for item in result.history: if item.status_code not in (301, 302): continue # GET methods can be redirected without issue if item.request.method == "GET": continue # Did we end-up with an https:// URL? location = item.headers.get("Location", None) if location and location.startswith("https://"): raise RedirectError(REDIRECT_MSG) def http_request( self, verb, path, query_data=None, post_data=None, 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 files (dict): The files to send to the server **kwargs: Extra options to send to the server (e.g. sudo) Returns: A requests result object. Raises: GitlabHttpError: When the return code is not 2xx """ query_data = query_data or {} url = self._build_url(path) params = {} utils.copy_dict(params, query_data) # Deal with kwargs: by default a user uses kwargs to send data to the # gitlab server, but this generates problems (python keyword conflicts # and python-gitlab/gitlab conflicts). # So we provide a `query_parameters` key: if it's there we use its dict # value as arguments for the gitlab server, and ignore the other # arguments, except pagination ones (per_page and page) if "query_parameters" in kwargs: utils.copy_dict(params, kwargs["query_parameters"]) for arg in ("per_page", "page"): if arg in kwargs: params[arg] = kwargs[arg] else: utils.copy_dict(params, kwargs) opts = self._get_session_opts(content_type="application/json") verify = opts.pop("verify") timeout = opts.pop("timeout") # If timeout was passed into kwargs, allow it to override the default timeout = kwargs.get("timeout", timeout) # We need to deal with json vs. data when uploading files if files: data = post_data json = None del opts["headers"]["Content-type"] else: json = post_data data = None # 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=json, data=data, params=params, files=files, **opts ) prepped = self.session.prepare_request(req) prepped.url = utils.sanitized_url(prepped.url) settings = self.session.merge_environment_settings( prepped.url, {}, streamed, verify, None ) # obey the rate limit by default obey_rate_limit = kwargs.get("obey_rate_limit", True) # do not retry transient errors by default retry_transient_errors = kwargs.get("retry_transient_errors", False) # set max_retries to 10 by default, disable by setting it to -1 max_retries = kwargs.get("max_retries", 10) cur_retries = 0 while True: result = self.session.send(prepped, timeout=timeout, **settings) self._check_redirects(result) if 200 <= result.status_code < 300: return result if (429 == result.status_code and obey_rate_limit) or ( result.status_code in [500, 502, 503, 504] and retry_transient_errors ): if max_retries == -1 or cur_retries < max_retries: wait_time = 2 ** cur_retries * 0.1 if "Retry-After" in result.headers: wait_time = int(result.headers["Retry-After"]) cur_retries += 1 time.sleep(wait_time) continue error_message = result.content try: error_json = result.json() for k in ("message", "error"): if k in error_json: error_message = error_json[k] except (KeyError, ValueError, TypeError): pass 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=None, streamed=False, raw=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 raw (bool): If True do not try to parse the output as json **kwargs: Extra options to send to the server (e.g. sudo) 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 """ query_data = query_data or {} result = self.http_request( "get", path, query_data=query_data, streamed=streamed, **kwargs ) if ( result.headers["Content-Type"] == "application/json" and not streamed and not raw ): 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=None, 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 options to send to the server (e.g. sudo, page, per_page) 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 """ query_data = query_data or {} # 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.pop("all", False) url = self._build_url(path) if get_all is True and as_list 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=None, post_data=None, 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) files (dict): The files to send to the server **kwargs: Extra options to send to the server (e.g. sudo) 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 """ query_data = query_data or {} post_data = post_data or {} 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=None, post_data=None, files=None, **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) files (dict): The files to send to the server **kwargs: Extra options to send to the server (e.g. sudo) 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 """ query_data = query_data or {} post_data = post_data or {} result = self.http_request( "put", path, query_data=query_data, post_data=post_data, files=files, **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 options to send to the server (e.g. sudo) Returns: The requests object. Raises: GitlabHttpError: When the return code is not 2xx """ return self.http_request("delete", path, **kwargs) @on_http_error(GitlabSearchError) def search(self, scope, search, **kwargs): """Search GitLab resources matching the provided string.' Args: scope (str): Scope of the search search (str): Search string **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabSearchError: If the server failed to perform the request Returns: GitlabList: A list of dicts describing the resources found. """ data = {"scope": scope, "search": search} return self.http_list("/search", query_data=data, **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=None, **kwargs): query_data = query_data or {} 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: pass if self._next_url and self._get_next is True: self._query(self._next_url) return self.next() raise StopIteration python-gitlab-2.0.1/gitlab/__main__.py000066400000000000000000000001011361651701000176170ustar00rootroot00000000000000import gitlab.cli __name__ == "__main__" and gitlab.cli.main() python-gitlab-2.0.1/gitlab/base.py000066400000000000000000000204351361651701000170250ustar00rootroot00000000000000# -*- 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 importlib 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.__dict__["_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 __eq__(self, other): if self.get_id() and other.get_id(): return self.get_id() == other.get_id() return super(RESTObject, self) == other def __ne__(self, other): if self.get_id() and other.get_id(): return self.get_id() != other.get_id() return super(RESTObject, self) != other def __hash__(self): if not self.get_id(): return super(RESTObject, self).__hash__() return hash(self.get_id()) 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 or not hasattr(self, self._id_attr): 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. Derived 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-2.0.1/gitlab/cli.py000066400000000000000000000137401361651701000166630ustar00rootroot00000000000000#!/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(add_help=True): parser = argparse.ArgumentParser( add_help=add_help, 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 _parse_value(v): if isinstance(v, str) and v.startswith("@"): # If the user-provided value starts with @, we try to read the file # path provided after @ as the real value. Exit on any error. try: with open(v[1:]) as fl: return fl.read() except Exception as e: sys.stderr.write("%s\n" % e) sys.exit(1) return v def main(): if "--version" in sys.argv: print(gitlab.__version__) sys.exit(0) parser = _get_base_parser(add_help=False) # This first parsing step is used to find the gitlab config to use, and # load the propermodule (v3 or v4) accordingly. At that point we don't have # any subparser setup (options, args) = parser.parse_known_args(sys.argv) try: config = gitlab.config.GitlabConfigParser(options.gitlab, options.config_file) except gitlab.config.ConfigError as e: if "--help" in sys.argv or "-h" in sys.argv: parser.print_help() sys.exit(0) sys.exit(e) cli_module = importlib.import_module("gitlab.v%s.cli" % config.api_version) # Now we build the entire set of subcommands and do the complete parsing parser = _get_parser(cli_module) try: import argcomplete argcomplete.autocomplete(parser) except Exception: pass 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.whaction what = args.what args = args.__dict__ # Remove CLI behavior-related args for item in ( "gitlab", "config_file", "verbose", "debug", "what", "whaction", "version", "output", ): args.pop(item) args = {k: _parse_value(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 or gl.job_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-2.0.1/gitlab/config.py000066400000000000000000000133301361651701000173540ustar00rootroot00000000000000# -*- 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 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 GitlabConfigMissingError(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 file_exist = False for file in _files: if os.path.exists(file): file_exist = True if not file_exist: raise GitlabConfigMissingError( "Config file not found. \nPlease create one in " "one of the following locations: {} \nor " "specify a config file using the '-c' parameter.".format( ", ".join(_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.job_token = None try: self.job_token = self._config.get(self.gitlab_id, "job_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 ("4",): raise GitlabDataError("Unsupported API version: %s" % self.api_version) self.per_page = None for section in ["global", self.gitlab_id]: try: self.per_page = self._config.getint(section, "per_page") except Exception: pass if self.per_page is not None and not 0 <= self.per_page <= 100: raise GitlabDataError("Unsupported per_page number: %s" % self.per_page) self.pagination = None try: self.pagination = self._config.get(self.gitlab_id, "pagination") except Exception: pass self.order_by = None try: self.order_by = self._config.get(self.gitlab_id, "order_by") except Exception: pass python-gitlab-2.0.1/gitlab/const.py000066400000000000000000000022571361651701000172430ustar00rootroot00000000000000# -*- 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 MAINTAINER_ACCESS = 40 MASTER_ACCESS = MAINTAINER_ACCESS 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-2.0.1/gitlab/exceptions.py000066400000000000000000000122071361651701000202720ustar00rootroot00000000000000# -*- 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 try: # if we receive str/bytes we try to convert to unicode/str to have # consistent message types (see #616) self.error_message = error_message.decode() except Exception: 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 RedirectError(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 GitlabDeactivateError(GitlabOperationError): pass class GitlabActivateError(GitlabOperationError): pass class GitlabSubscribeError(GitlabOperationError): pass class GitlabUnsubscribeError(GitlabOperationError): pass class GitlabMRForbiddenError(GitlabOperationError): pass class GitlabMRApprovalError(GitlabOperationError): pass class GitlabMRRebaseError(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 class GitlabSearchError(GitlabOperationError): pass class GitlabStopError(GitlabOperationError): pass class GitlabMarkdownError(GitlabOperationError): pass class GitlabVerifyError(GitlabOperationError): pass class GitlabRenderError(GitlabOperationError): pass class GitlabRepairError(GitlabOperationError): pass class GitlabLicenseError(GitlabOperationError): pass 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-2.0.1/gitlab/mixins.py000066400000000000000000000551061361651701000174250ustar00rootroot00000000000000# -*- 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 from gitlab import types as g_types from gitlab import utils 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 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 = utils.clean_str_id(id) 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 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) if server_data is None: return None return self._obj_cls(self, server_data) class RefreshMixin(object): @exc.on_http_error(exc.GitlabGetError) def refresh(self, **kwargs): """Refresh a single object from server. Args: **kwargs: Extra options to send to the server (e.g. sudo) Returns None (updates the object) Raises: GitlabAuthenticationError: If authentication is not correct GitlabGetError: If the server cannot perform the request """ if self._id_attr: path = "%s/%s" % (self.manager.path, self.id) else: path = self.manager.path server_data = self.manager.gitlab.http_get(path, **kwargs) self._update_attrs(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 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 """ # Duplicate data to avoid messing with what the user sent us data = kwargs.copy() if self.gitlab.per_page: data.setdefault("per_page", self.gitlab.per_page) # global keyset pagination if self.gitlab.pagination: data.setdefault("pagination", self.gitlab.pagination) if self.gitlab.order_by: data.setdefault("order_by", self.gitlab.order_by) # We get the attributes that need some special transformation types = getattr(self, "_types", {}) if types: for attr_name, type_cls in types.items(): if attr_name in data.keys(): type_obj = type_cls(data[attr_name]) data[attr_name] = type_obj.get_for_api() # Allow to overwrite the path, handy for custom listings path = data.pop("path", self.path) obj = self.gitlab.http_list(path, **data) if isinstance(obj, list): return [self._obj_cls(self, item) for item in obj] else: return base.RESTObjectList(self, self._obj_cls, obj) 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 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) files = {} # We get the attributes that need some special transformation types = getattr(self, "_types", {}) if types: # Duplicate data to avoid messing with what the user sent us data = data.copy() for attr_name, type_cls in types.items(): if attr_name in data.keys(): type_obj = type_cls(data[attr_name]) # if the type if FileAttribute we need to pass the data as # file if issubclass(type_cls, g_types.FileAttribute): k = type_obj.get_file_name(attr_name) files[attr_name] = (k, data.pop(attr_name)) else: data[attr_name] = type_obj.get_for_api() # Handle specific URL for creation path = kwargs.pop("path", self.path) server_data = self.gitlab.http_post(path, post_data=data, files=files, **kwargs) return self._obj_cls(self, server_data) class UpdateMixin(object): def _check_missing_update_attrs(self, data): required, optional = self.get_update_attrs() # Remove the id field from the required list as it was previously moved to the http path. required = tuple(filter(lambda k: k != self._obj_cls._id_attr, required)) 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())) def _get_update_method(self): """Return the HTTP method to use. Returns: object: http_put (default) or http_post """ if getattr(self, "_update_uses_post", False): http_method = self.gitlab.http_post else: http_method = self.gitlab.http_put return http_method @exc.on_http_error(exc.GitlabUpdateError) def update(self, id=None, new_data=None, **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 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 """ new_data = new_data or {} if id is None: path = self.path else: path = "%s/%s" % (self.path, id) self._check_missing_update_attrs(new_data) files = {} # We get the attributes that need some special transformation types = getattr(self, "_types", {}) if types: # Duplicate data to avoid messing with what the user sent us new_data = new_data.copy() for attr_name, type_cls in types.items(): if attr_name in new_data.keys(): type_obj = type_cls(new_data[attr_name]) # if the type if FileAttribute we need to pass the data as # file if issubclass(type_cls, g_types.FileAttribute): k = type_obj.get_file_name(attr_name) files[attr_name] = (k, new_data.pop(attr_name)) else: new_data[attr_name] = type_obj.get_for_api() http_method = self._get_update_method() return http_method(path, post_data=new_data, files=files, **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, utils.clean_str_id(key)) 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 server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabDeleteError: If the server cannot perform the request """ if id is None: path = self.path else: if not isinstance(id, int): id = utils.clean_str_id(id) 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 UserAgentDetailMixin(object): @cli.register_custom_action(("Snippet", "ProjectSnippet", "ProjectIssue")) @exc.on_http_error(exc.GitlabGetError) def user_agent_detail(self, **kwargs): """Get the 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 server cannot perform the request """ path = "%s/%s/user_agent_detail" % (self.manager.path, self.get_id()) return self.manager.gitlab.http_get(path, **kwargs) 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", "GroupLabel") ) @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", "GroupLabel") ) @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 """ # Use the existing time_stats attribute if it exist, otherwise make an # API call if "time_stats" in self.attributes: return self.attributes["time_stats"] 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/reset_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) class ParticipantsMixin(object): @cli.register_custom_action(("ProjectMergeRequest", "ProjectIssue")) @exc.on_http_error(exc.GitlabListError) def participants(self, **kwargs): """List the 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 BadgeRenderMixin(object): @cli.register_custom_action( ("GroupBadgeManager", "ProjectBadgeManager"), ("link_url", "image_url") ) @exc.on_http_error(exc.GitlabRenderError) def render(self, link_url, image_url, **kwargs): """Preview link_url and image_url after interpolation. Args: link_url (str): URL of the badge link image_url (str): URL of the badge image **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabRenderError: If the rendering failed Returns: dict: The rendering properties """ path = "%s/render" % self.path data = {"link_url": link_url, "image_url": image_url} return self.gitlab.http_get(path, data, **kwargs) python-gitlab-2.0.1/gitlab/tests/000077500000000000000000000000001361651701000166775ustar00rootroot00000000000000python-gitlab-2.0.1/gitlab/tests/__init__.py000066400000000000000000000000001361651701000207760ustar00rootroot00000000000000python-gitlab-2.0.1/gitlab/tests/data/000077500000000000000000000000001361651701000176105ustar00rootroot00000000000000python-gitlab-2.0.1/gitlab/tests/data/todo.json000066400000000000000000000050671361651701000214600ustar00rootroot00000000000000[ { "id": 102, "project": { "id": 2, "name": "Gitlab Ce", "name_with_namespace": "Gitlab Org / Gitlab Ce", "path": "gitlab-ce", "path_with_namespace": "gitlab-org/gitlab-ce" }, "author": { "name": "Administrator", "username": "root", "id": 1, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "web_url": "https://gitlab.example.com/root" }, "action_name": "marked", "target_type": "MergeRequest", "target": { "id": 34, "iid": 7, "project_id": 2, "title": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.", "description": "Et ea et omnis illum cupiditate. Dolor aspernatur tenetur ducimus facilis est nihil. Quo esse cupiditate molestiae illo corrupti qui quidem dolor.", "state": "opened", "created_at": "2016-06-17T07:49:24.419Z", "updated_at": "2016-06-17T07:52:43.484Z", "target_branch": "tutorials_git_tricks", "source_branch": "DNSBL_docs", "upvotes": 0, "downvotes": 0, "author": { "name": "Maxie Medhurst", "username": "craig_rutherford", "id": 12, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon", "web_url": "https://gitlab.example.com/craig_rutherford" }, "assignee": { "name": "Administrator", "username": "root", "id": 1, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "web_url": "https://gitlab.example.com/root" }, "source_project_id": 2, "target_project_id": 2, "labels": [], "work_in_progress": false, "milestone": { "id": 32, "iid": 2, "project_id": 2, "title": "v1.0", "description": "Assumenda placeat ea voluptatem voluptate qui.", "state": "active", "created_at": "2016-06-17T07:47:34.163Z", "updated_at": "2016-06-17T07:47:34.163Z", "due_date": null }, "merge_when_pipeline_succeeds": false, "merge_status": "cannot_be_merged", "subscribed": true, "user_notes_count": 7 }, "target_url": "https://gitlab.example.com/gitlab-org/gitlab-ce/merge_requests/7", "body": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.", "state": "pending", "created_at": "2016-06-17T07:52:35.225Z" } ] python-gitlab-2.0.1/gitlab/tests/objects/000077500000000000000000000000001361651701000203305ustar00rootroot00000000000000python-gitlab-2.0.1/gitlab/tests/objects/__init__.py000066400000000000000000000000001361651701000224270ustar00rootroot00000000000000python-gitlab-2.0.1/gitlab/tests/objects/test_application.py000066400000000000000000000107421361651701000242500ustar00rootroot00000000000000import unittest import gitlab import os import pickle import tempfile import json import unittest import requests from gitlab import * # noqa from gitlab.v4.objects import * # noqa from httmock import HTTMock, urlmatch, response # noqa headers = {"content-type": "application/json"} class TestApplicationAppearance(unittest.TestCase): def setUp(self): self.gl = Gitlab( "http://localhost", private_token="private_token", ssl_verify=True, api_version="4", ) self.title = "GitLab Test Instance" self.new_title = "new-title" self.description = "gitlab-test.example.com" self.new_description = "new-description" def test_get_update_appearance(self): @urlmatch( scheme="http", netloc="localhost", path="/api/v4/application/appearance", method="get", ) def resp_get_appearance(url, request): content = """{ "title": "%s", "description": "%s", "logo": "/uploads/-/system/appearance/logo/1/logo.png", "header_logo": "/uploads/-/system/appearance/header_logo/1/header.png", "favicon": "/uploads/-/system/appearance/favicon/1/favicon.png", "new_project_guidelines": "Please read the FAQs for help.", "header_message": "", "footer_message": "", "message_background_color": "#e75e40", "message_font_color": "#ffffff", "email_header_and_footer_enabled": false}""" % ( self.title, self.description, ) content = content.encode("utf-8") return response(200, content, headers, None, 25, request) @urlmatch( scheme="http", netloc="localhost", path="/api/v4/application/appearance", method="put", ) def resp_update_appearance(url, request): content = """{ "title": "%s", "description": "%s", "logo": "/uploads/-/system/appearance/logo/1/logo.png", "header_logo": "/uploads/-/system/appearance/header_logo/1/header.png", "favicon": "/uploads/-/system/appearance/favicon/1/favicon.png", "new_project_guidelines": "Please read the FAQs for help.", "header_message": "", "footer_message": "", "message_background_color": "#e75e40", "message_font_color": "#ffffff", "email_header_and_footer_enabled": false}""" % ( self.new_title, self.new_description, ) content = content.encode("utf-8") return response(200, content, headers, None, 25, request) with HTTMock(resp_get_appearance), HTTMock(resp_update_appearance): appearance = self.gl.appearance.get() self.assertEqual(appearance.title, self.title) self.assertEqual(appearance.description, self.description) appearance.title = self.new_title appearance.description = self.new_description appearance.save() self.assertEqual(appearance.title, self.new_title) self.assertEqual(appearance.description, self.new_description) def test_update_appearance(self): @urlmatch( scheme="http", netloc="localhost", path="/api/v4/application/appearance", method="put", ) def resp_update_appearance(url, request): content = """{ "title": "%s", "description": "%s", "logo": "/uploads/-/system/appearance/logo/1/logo.png", "header_logo": "/uploads/-/system/appearance/header_logo/1/header.png", "favicon": "/uploads/-/system/appearance/favicon/1/favicon.png", "new_project_guidelines": "Please read the FAQs for help.", "header_message": "", "footer_message": "", "message_background_color": "#e75e40", "message_font_color": "#ffffff", "email_header_and_footer_enabled": false}""" % ( self.new_title, self.new_description, ) content = content.encode("utf-8") return response(200, content, headers, None, 25, request) with HTTMock(resp_update_appearance): resp = self.gl.appearance.update( title=self.new_title, description=self.new_description ) python-gitlab-2.0.1/gitlab/tests/objects/test_projects.py000066400000000000000000000110251361651701000235710ustar00rootroot00000000000000import unittest import gitlab import os import pickle import tempfile import json import unittest import requests from gitlab import * # noqa from gitlab.v4.objects import * # noqa from httmock import HTTMock, urlmatch, response # noqa headers = {"content-type": "application/json"} class TestProjectSnippets(unittest.TestCase): def setUp(self): self.gl = Gitlab( "http://localhost", private_token="private_token", ssl_verify=True, api_version=4, ) def test_list_project_snippets(self): title = "Example Snippet Title" visibility = "private" @urlmatch( scheme="http", netloc="localhost", path="/api/v4/projects/1/snippets", method="get", ) def resp_list_snippet(url, request): content = """[{ "title": "%s", "description": "More verbose snippet description", "file_name": "example.txt", "content": "source code with multiple lines", "visibility": "%s"}]""" % ( title, visibility, ) content = content.encode("utf-8") return response(200, content, headers, None, 25, request) with HTTMock(resp_list_snippet): snippets = self.gl.projects.get(1, lazy=True).snippets.list() self.assertEqual(len(snippets), 1) self.assertEqual(snippets[0].title, title) self.assertEqual(snippets[0].visibility, visibility) def test_get_project_snippets(self): title = "Example Snippet Title" visibility = "private" @urlmatch( scheme="http", netloc="localhost", path="/api/v4/projects/1/snippets/1", method="get", ) def resp_get_snippet(url, request): content = """{ "title": "%s", "description": "More verbose snippet description", "file_name": "example.txt", "content": "source code with multiple lines", "visibility": "%s"}""" % ( title, visibility, ) content = content.encode("utf-8") return response(200, content, headers, None, 25, request) with HTTMock(resp_get_snippet): snippet = self.gl.projects.get(1, lazy=True).snippets.get(1) self.assertEqual(snippet.title, title) self.assertEqual(snippet.visibility, visibility) def test_create_update_project_snippets(self): title = "Example Snippet Title" visibility = "private" @urlmatch( scheme="http", netloc="localhost", path="/api/v4/projects/1/snippets", method="put", ) def resp_update_snippet(url, request): content = """{ "title": "%s", "description": "More verbose snippet description", "file_name": "example.txt", "content": "source code with multiple lines", "visibility": "%s"}""" % ( title, visibility, ) content = content.encode("utf-8") return response(200, content, headers, None, 25, request) @urlmatch( scheme="http", netloc="localhost", path="/api/v4/projects/1/snippets", method="post", ) def resp_create_snippet(url, request): content = """{ "title": "%s", "description": "More verbose snippet description", "file_name": "example.txt", "content": "source code with multiple lines", "visibility": "%s"}""" % ( title, visibility, ) content = content.encode("utf-8") return response(200, content, headers, None, 25, request) with HTTMock(resp_create_snippet, resp_update_snippet): snippet = self.gl.projects.get(1, lazy=True).snippets.create( { "title": title, "file_name": title, "content": title, "visibility": visibility, } ) self.assertEqual(snippet.title, title) self.assertEqual(snippet.visibility, visibility) title = "new-title" snippet.title = title snippet.save() self.assertEqual(snippet.title, title) self.assertEqual(snippet.visibility, visibility) python-gitlab-2.0.1/gitlab/tests/test_base.py000066400000000000000000000117531361651701000212310ustar00rootroot00000000000000# -*- 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 import 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 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) pickled2 = pickle.dumps(unpickled) 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"}) obj.id = 42 self.assertIsInstance(obj.fakes, FakeManager) self.assertEqual(obj.fakes.gitlab, self.gitlab) self.assertEqual(obj.fakes._parent, obj) def test_equality(self): obj1 = FakeObject(self.manager, {"id": "foo"}) obj2 = FakeObject(self.manager, {"id": "foo", "other_attr": "bar"}) self.assertEqual(obj1, obj2) def test_equality_custom_id(self): class OtherFakeObject(FakeObject): _id_attr = "foo" obj1 = OtherFakeObject(self.manager, {"foo": "bar"}) obj2 = OtherFakeObject(self.manager, {"foo": "bar", "other_attr": "baz"}) self.assertEqual(obj1, obj2) def test_inequality(self): obj1 = FakeObject(self.manager, {"id": "foo"}) obj2 = FakeObject(self.manager, {"id": "bar"}) self.assertNotEqual(obj1, obj2) def test_inequality_no_id(self): obj1 = FakeObject(self.manager, {"attr1": "foo"}) obj2 = FakeObject(self.manager, {"attr1": "bar"}) self.assertNotEqual(obj1, obj2) python-gitlab-2.0.1/gitlab/tests/test_cli.py000066400000000000000000000114461361651701000210650ustar00rootroot00000000000000#!/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 . import argparse import os import tempfile import unittest import io try: from contextlib import redirect_stderr # noqa: H302 except ImportError: from contextlib import contextmanager # noqa: H302 import sys @contextmanager def redirect_stderr(new_target): old_target, sys.stderr = sys.stderr, new_target yield sys.stderr = old_target from gitlab import cli import gitlab.v4.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): fl = io.StringIO() with redirect_stderr(fl): with self.assertRaises(SystemExit) as test: cli.die("foobar") self.assertEqual(fl.getvalue(), "foobar\n") self.assertEqual(test.exception.code, 1) def test_parse_value(self): ret = cli._parse_value("foobar") self.assertEqual(ret, "foobar") ret = cli._parse_value(True) self.assertEqual(ret, True) ret = cli._parse_value(1) self.assertEqual(ret, 1) ret = cli._parse_value(None) self.assertEqual(ret, None) fd, temp_path = tempfile.mkstemp() os.write(fd, b"content") os.close(fd) ret = cli._parse_value("@%s" % temp_path) self.assertEqual(ret, "content") os.unlink(temp_path) fl = io.StringIO() with redirect_stderr(fl): with self.assertRaises(SystemExit) as exc: cli._parse_value("@/thisfileprobablydoesntexist") self.assertEqual( fl.getvalue(), "[Errno 2] No such file or directory:" " '/thisfileprobablydoesntexist'\n", ) self.assertEqual(exc.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 TestV4CLI(unittest.TestCase): def test_parse_args(self): parser = cli._get_parser(gitlab.v4.cli) args = parser.parse_args(["project", "list"]) self.assertEqual(args.what, "project") self.assertEqual(args.whaction, "list") def test_parser(self): parser = cli._get_parser(gitlab.v4.cli) subparsers = next( action for action in parser._actions if isinstance(action, argparse._SubParsersAction) ) self.assertIsNotNone(subparsers) self.assertIn("project", subparsers.choices) user_subparsers = next( action for action in subparsers.choices["project"]._actions if isinstance(action, argparse._SubParsersAction) ) 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("archive", user_subparsers.choices) self.assertIn("unarchive", user_subparsers.choices) actions = user_subparsers.choices["create"]._option_string_actions self.assertFalse(actions["--description"].required) user_subparsers = next( action for action in subparsers.choices["group"]._actions if isinstance(action, argparse._SubParsersAction) ) actions = user_subparsers.choices["create"]._option_string_actions self.assertTrue(actions["--name"].required) python-gitlab-2.0.1/gitlab/tests/test_config.py000066400000000000000000000123051361651701000215560ustar00rootroot00000000000000# -*- 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 . import unittest import mock import io 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 per_page = 50 [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 [four] url = http://four.url private_token = ABCDEF per_page = 200 """ class TestConfigParser(unittest.TestCase): @mock.patch("os.path.exists") def test_missing_config(self, path_exists): path_exists.return_value = False with self.assertRaises(config.GitlabConfigMissingError): config.GitlabConfigParser("test") @mock.patch("os.path.exists") @mock.patch("builtins.open") def test_invalid_id(self, m_open, path_exists): fd = io.StringIO(no_default_config) fd.close = mock.Mock(return_value=None) m_open.return_value = fd path_exists.return_value = True config.GitlabConfigParser("there") self.assertRaises(config.GitlabIDError, config.GitlabConfigParser) fd = io.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("os.path.exists") @mock.patch("builtins.open") def test_invalid_data(self, m_open, path_exists): fd = io.StringIO(missing_attr_config) fd.close = mock.Mock(return_value=None, side_effect=lambda: fd.seek(0)) m_open.return_value = fd path_exists.return_value = True config.GitlabConfigParser("one") config.GitlabConfigParser("one") self.assertRaises( config.GitlabDataError, config.GitlabConfigParser, gitlab_id="two" ) self.assertRaises( config.GitlabDataError, config.GitlabConfigParser, gitlab_id="three" ) with self.assertRaises(config.GitlabDataError) as emgr: config.GitlabConfigParser("four") self.assertEqual("Unsupported per_page number: 200", emgr.exception.args[0]) @mock.patch("os.path.exists") @mock.patch("builtins.open") def test_valid_data(self, m_open, path_exists): fd = io.StringIO(valid_config) fd.close = mock.Mock(return_value=None) m_open.return_value = fd path_exists.return_value = True 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) self.assertIsNone(cp.per_page) fd = io.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 = io.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) self.assertEqual(50, cp.per_page) fd = io.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-2.0.1/gitlab/tests/test_gitlab.py000066400000000000000000001021501361651701000215510ustar00rootroot00000000000000# -*- 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 . import os import pickle import tempfile import json import unittest from httmock import HTTMock # noqa from httmock import response # noqa from httmock import urlmatch # noqa import requests import gitlab from gitlab import * # noqa from gitlab.v4.objects import * # noqa valid_config = b"""[global] default = one ssl_verify = true timeout = 2 [one] url = http://one.url private_token = ABCDEF """ 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 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") def test_all_omitted_when_as_list(self): @urlmatch(scheme="http", netloc="localhost", path="/api/v4/tests", method="get") def resp(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): result = self.gl.http_list("/tests", as_list=False, all=True) self.assertIsInstance(result, GitlabList) 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 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.job_token, None) self.assertEqual(gl._http_auth, None) self.assertNotIn("Authorization", gl.headers) self.assertEqual(gl.headers["PRIVATE-TOKEN"], "private_token") self.assertNotIn("JOB-TOKEN", 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.job_token, None) self.assertEqual(gl._http_auth, None) self.assertEqual(gl.headers["Authorization"], "Bearer oauth_token") self.assertNotIn("PRIVATE-TOKEN", gl.headers) self.assertNotIn("JOB-TOKEN", gl.headers) def test_job_token_auth(self): gl = Gitlab("http://localhost", job_token="CI_JOB_TOKEN", api_version="4") self.assertEqual(gl.private_token, None) self.assertEqual(gl.oauth_token, None) self.assertEqual(gl.job_token, "CI_JOB_TOKEN") self.assertEqual(gl._http_auth, None) self.assertNotIn("Authorization", gl.headers) self.assertNotIn("PRIVATE-TOKEN", gl.headers) self.assertEqual(gl.headers["JOB-TOKEN"], "CI_JOB_TOKEN") 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.assertEqual(gl.job_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", ssl_verify=True, api_version=4, ) 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_token_auth(self, callback=None): name = "username" id_ = 1 @urlmatch(scheme="http", netloc="localhost", path="/api/v4/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): self.gl.auth() self.assertEqual(self.gl.user.username, name) self.assertEqual(self.gl.user.id, id_) self.assertIsInstance(self.gl.user, CurrentUser) def test_hooks(self): @urlmatch( scheme="http", netloc="localhost", path="/api/v4/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.assertIsInstance(data, Hook) self.assertEqual(data.url, "testurl") self.assertEqual(data.id, 1) def test_projects(self): @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) with HTTMock(resp_get_project): data = self.gl.projects.get(1) self.assertIsInstance(data, Project) self.assertEqual(data.name, "name") self.assertEqual(data.id, 1) def test_project_environments(self): @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/1/environments/1", method="get", ) def resp_get_environment(url, request): headers = {"content-type": "application/json"} content = '{"name": "environment_name", "id": 1, "last_deployment": "sometime"}'.encode( "utf-8" ) return response(200, content, headers, None, 5, request) with HTTMock(resp_get_project, resp_get_environment): project = self.gl.projects.get(1) environment = project.environments.get(1) self.assertIsInstance(environment, ProjectEnvironment) self.assertEqual(environment.id, 1) self.assertEqual(environment.last_deployment, "sometime") self.assertEqual(environment.name, "environment_name") def test_project_additional_statistics(self): @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/1/statistics", method="get", ) def resp_get_environment(url, request): headers = {"content-type": "application/json"} content = """{"fetches": {"total": 50, "days": [{"count": 10, "date": "2018-01-10"}]}}""".encode( "utf-8" ) return response(200, content, headers, None, 5, request) with HTTMock(resp_get_project, resp_get_environment): project = self.gl.projects.get(1) statistics = project.additionalstatistics.get() self.assertIsInstance(statistics, ProjectAdditionalStatistics) self.assertEqual(statistics.fetches["total"], 50) def test_project_issues_statistics(self): @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/1/issues_statistics", method="get", ) def resp_get_environment(url, request): headers = {"content-type": "application/json"} content = """{"statistics": {"counts": {"all": 20, "closed": 5, "opened": 15}}}""".encode( "utf-8" ) return response(200, content, headers, None, 5, request) with HTTMock(resp_get_project, resp_get_environment): project = self.gl.projects.get(1) statistics = project.issuesstatistics.get() self.assertIsInstance(statistics, ProjectIssuesStatistics) self.assertEqual(statistics.statistics["counts"]["all"], 20) def test_groups(self): @urlmatch( scheme="http", netloc="localhost", path="/api/v4/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.assertIsInstance(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/v4/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.list() self.assertEqual(data[1].id, 2) self.assertEqual(data[1].name, "other_name") @urlmatch(scheme="http", netloc="localhost", path="/api/v4/users/1", method="get") def resp_get_user(self, 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) def test_users(self): with HTTMock(self.resp_get_user): user = self.gl.users.get(1) self.assertIsInstance(user, User) self.assertEqual(user.name, "name") self.assertEqual(user.id, 1) def test_user_status(self): @urlmatch( scheme="http", netloc="localhost", path="/api/v4/users/1/status", method="get", ) def resp_get_user_status(url, request): headers = {"content-type": "application/json"} content = '{"message": "test", "message_html": "

Message

", "emoji": "thumbsup"}' content = content.encode("utf-8") return response(200, content, headers, None, 5, request) with HTTMock(self.resp_get_user): user = self.gl.users.get(1) with HTTMock(resp_get_user_status): status = user.status.get() self.assertIsInstance(status, UserStatus) self.assertEqual(status.message, "test") self.assertEqual(status.emoji, "thumbsup") def test_todo(self): with open(os.path.dirname(__file__) + "/data/todo.json", "r") as json_file: todo_content = json_file.read() json_content = json.loads(todo_content) encoded_content = todo_content.encode("utf-8") @urlmatch(scheme="http", netloc="localhost", path="/api/v4/todos", method="get") def resp_get_todo(url, request): headers = {"content-type": "application/json"} return response(200, encoded_content, headers, None, 5, request) @urlmatch( scheme="http", netloc="localhost", path="/api/v4/todos/102/mark_as_done", method="post", ) def resp_mark_as_done(url, request): headers = {"content-type": "application/json"} single_todo = json.dumps(json_content[0]) content = single_todo.encode("utf-8") return response(200, content, headers, None, 5, request) with HTTMock(resp_get_todo): todo = self.gl.todos.list()[0] self.assertIsInstance(todo, Todo) self.assertEqual(todo.id, 102) self.assertEqual(todo.target_type, "MergeRequest") self.assertEqual(todo.target["assignee"]["username"], "root") with HTTMock(resp_mark_as_done): todo.mark_as_done() def test_todo_mark_all_as_done(self): @urlmatch( scheme="http", netloc="localhost", path="/api/v4/todos/mark_as_done", method="post", ) def resp_mark_all_as_done(url, request): headers = {"content-type": "application/json"} return response(204, {}, headers, None, 5, request) with HTTMock(resp_mark_all_as_done): self.gl.todos.mark_all_as_done() def test_deployment(self): content = '{"id": 42, "status": "success", "ref": "master"}' json_content = json.loads(content) @urlmatch( scheme="http", netloc="localhost", path="/api/v4/projects/1/deployments", method="post", ) def resp_deployment_create(url, request): headers = {"content-type": "application/json"} return response(200, json_content, headers, None, 5, request) @urlmatch( scheme="http", netloc="localhost", path="/api/v4/projects/1/deployments/42", method="put", ) def resp_deployment_update(url, request): headers = {"content-type": "application/json"} return response(200, json_content, headers, None, 5, request) with HTTMock(resp_deployment_create): deployment = self.gl.projects.get(1, lazy=True).deployments.create( { "environment": "Test", "sha": "1agf4gs", "ref": "master", "tag": False, "status": "created", } ) self.assertEqual(deployment.id, 42) self.assertEqual(deployment.status, "success") self.assertEqual(deployment.ref, "master") with HTTMock(resp_deployment_update): json_content["status"] = "failed" deployment.status = "failed" deployment.save() self.assertEqual(deployment.status, "failed") def test_user_activate_deactivate(self): @urlmatch( scheme="http", netloc="localhost", path="/api/v4/users/1/activate", method="post", ) def resp_activate(url, request): headers = {"content-type": "application/json"} return response(201, {}, headers, None, 5, request) @urlmatch( scheme="http", netloc="localhost", path="/api/v4/users/1/deactivate", method="post", ) def resp_deactivate(url, request): headers = {"content-type": "application/json"} return response(201, {}, headers, None, 5, request) with HTTMock(resp_activate), HTTMock(resp_deactivate): self.gl.users.get(1, lazy=True).activate() self.gl.users.get(1, lazy=True).deactivate() def test_update_submodule(self): @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/1/repository/submodules/foo%2Fbar", method="put", ) def resp_update_submodule(url, request): headers = {"content-type": "application/json"} content = """{ "id": "ed899a2f4b50b4370feeea94676502b42383c746", "short_id": "ed899a2f4b5", "title": "Message", "author_name": "Author", "author_email": "author@example.com", "committer_name": "Author", "committer_email": "author@example.com", "created_at": "2018-09-20T09:26:24.000-07:00", "message": "Message", "parent_ids": [ "ae1d9fb46aa2b07ee9836d49862ec4e2c46fbbba" ], "committed_date": "2018-09-20T09:26:24.000-07:00", "authored_date": "2018-09-20T09:26:24.000-07:00", "status": null}""" content = content.encode("utf-8") return response(200, content, headers, None, 5, request) with HTTMock(resp_get_project): project = self.gl.projects.get(1) self.assertIsInstance(project, Project) self.assertEqual(project.name, "name") self.assertEqual(project.id, 1) with HTTMock(resp_update_submodule): ret = project.update_submodule( submodule="foo/bar", branch="master", commit_sha="4c3674f66071e30b3311dac9b9ccc90502a72664", commit_message="Message", ) self.assertIsInstance(ret, dict) self.assertEqual(ret["message"], "Message") self.assertEqual(ret["id"], "ed899a2f4b50b4370feeea94676502b42383c746") def test_import_github(self): @urlmatch( scheme="http", netloc="localhost", path="/api/v4/import/github", method="post", ) def resp_import_github(url, request): headers = {"content-type": "application/json"} content = """{ "id": 27, "name": "my-repo", "full_path": "/root/my-repo", "full_name": "Administrator / my-repo" }""" content = content.encode("utf-8") return response(200, content, headers, None, 25, request) with HTTMock(resp_import_github): base_path = "/root" name = "my-repo" ret = self.gl.projects.import_github("githubkey", 1234, base_path, name) self.assertIsInstance(ret, dict) self.assertEqual(ret["name"], name) self.assertEqual(ret["full_path"], "/".join((base_path, name))) self.assertTrue(ret["full_name"].endswith(name)) def _default_config(self): fd, temp_path = tempfile.mkstemp() os.write(fd, valid_config) os.close(fd) return temp_path def test_from_config(self): config_path = self._default_config() gitlab.Gitlab.from_config("one", [config_path]) os.unlink(config_path) def test_subclass_from_config(self): class MyGitlab(gitlab.Gitlab): pass config_path = self._default_config() gl = MyGitlab.from_config("one", [config_path]) self.assertIsInstance(gl, MyGitlab) os.unlink(config_path) python-gitlab-2.0.1/gitlab/tests/test_mixins.py000066400000000000000000000356641361651701000216350ustar00rootroot00000000000000# -*- 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 . import 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")) def test_user_agent_detail_mixin(self): class O(UserAgentDetailMixin): pass obj = O() self.assertTrue(hasattr(obj, "user_agent_detail")) 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_refresh_mixin(self): class O(RefreshMixin, FakeObject): 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 = FakeManager(self.gl) obj = O(mgr, {"id": 42}) res = obj.refresh() self.assertIsNone(res) 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_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-2.0.1/gitlab/tests/test_types.py000066400000000000000000000037171361651701000214640ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Copyright (C) 2018 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 unittest from gitlab import types class TestGitlabAttribute(unittest.TestCase): def test_all(self): o = types.GitlabAttribute("whatever") self.assertEqual("whatever", o.get()) o.set_from_cli("whatever2") self.assertEqual("whatever2", o.get()) self.assertEqual("whatever2", o.get_for_api()) o = types.GitlabAttribute() self.assertEqual(None, o._value) class TestListAttribute(unittest.TestCase): def test_list_input(self): o = types.ListAttribute() o.set_from_cli("foo,bar,baz") self.assertEqual(["foo", "bar", "baz"], o.get()) o.set_from_cli("foo") self.assertEqual(["foo"], o.get()) def test_empty_input(self): o = types.ListAttribute() o.set_from_cli("") self.assertEqual([], o.get()) o.set_from_cli(" ") self.assertEqual([], o.get()) def test_get_for_api(self): o = types.ListAttribute() o.set_from_cli("foo,bar,baz") self.assertEqual("foo,bar,baz", o.get_for_api()) class TestLowercaseStringAttribute(unittest.TestCase): def test_get_for_api(self): o = types.LowercaseStringAttribute("FOO") self.assertEqual("foo", o.get_for_api()) python-gitlab-2.0.1/gitlab/tests/test_utils.py000066400000000000000000000026251361651701000214550ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Copyright (C) 2019 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 unittest from gitlab import utils class TestUtils(unittest.TestCase): def test_clean_str_id(self): src = "nothing_special" dest = "nothing_special" self.assertEqual(dest, utils.clean_str_id(src)) src = "foo#bar/baz/" dest = "foo%23bar%2Fbaz%2F" self.assertEqual(dest, utils.clean_str_id(src)) def test_sanitized_url(self): src = "http://localhost/foo/bar" dest = "http://localhost/foo/bar" self.assertEqual(dest, utils.sanitized_url(src)) src = "http://localhost/foo.bar.baz" dest = "http://localhost/foo%2Ebar%2Ebaz" self.assertEqual(dest, utils.sanitized_url(src)) python-gitlab-2.0.1/gitlab/types.py000066400000000000000000000032301361651701000172510ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Copyright (C) 2018 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 GitlabAttribute(object): def __init__(self, value=None): self._value = value def get(self): return self._value def set_from_cli(self, cli_value): self._value = cli_value def get_for_api(self): return self._value class ListAttribute(GitlabAttribute): def set_from_cli(self, cli_value): if not cli_value.strip(): self._value = [] else: self._value = [item.strip() for item in cli_value.split(",")] def get_for_api(self): return ",".join(self._value) class LowercaseStringAttribute(GitlabAttribute): def get_for_api(self): return str(self._value).lower() class FileAttribute(GitlabAttribute): def get_file_name(self, attr_name=None): return attr_name class ImageAttribute(FileAttribute): def get_file_name(self, attr_name=None): return "%s.png" % attr_name if attr_name else "image.png" python-gitlab-2.0.1/gitlab/utils.py000066400000000000000000000033621361651701000172530ustar00rootroot00000000000000# -*- 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 urllib.parse import urlparse 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) def copy_dict(dest, src): for k, v in src.items(): if isinstance(v, dict): # Transform dict values to 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 def clean_str_id(id): return id.replace("/", "%2F").replace("#", "%23") def sanitized_url(url): parsed = urlparse(url) new_path = parsed.path.replace(".", "%2E") return parsed._replace(path=new_path).geturl() python-gitlab-2.0.1/gitlab/v4/000077500000000000000000000000001361651701000160665ustar00rootroot00000000000000python-gitlab-2.0.1/gitlab/v4/__init__.py000066400000000000000000000000001361651701000201650ustar00rootroot00000000000000python-gitlab-2.0.1/gitlab/v4/cli.py000066400000000000000000000370661361651701000172230ustar00rootroot00000000000000#!/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 sys 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) types = getattr(self.mgr_cls, "_types", {}) if types: for attr_name, type_cls in types.items(): if attr_name in self.args.keys(): obj = type_cls() obj.set_from_cli(self.args[attr_name]) self.args[attr_name] = obj.get() def __call__(self): # Check for a method that matches object + action method = "do_%s_%s" % (self.what, self.action) if hasattr(self, method): return getattr(self, method)() # Fallback to standard actions (get, list, create, ...) method = "do_%s" % self.action if hasattr(self, method): return getattr(self, method)() # Finally try to find custom methods 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_project_export_download(self): try: project = self.gl.projects.get(int(self.args["project_id"]), lazy=True) data = project.exports.get().download() if hasattr(sys.stdout, "buffer"): # python3 sys.stdout.buffer.write(data) else: sys.stdout.write(data) except Exception as e: cli.die("Impossible to download the export", e) 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": if cls._id_attr is not None: 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] ] [ sub_parser_action.add_argument( "--%s" % x.replace("_", "-"), required=False ) for x in mgr_cls._create_attrs[1] ] 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( title="action", dest="whaction", help="Action to execute." ) _populate_sub_parser_by_class(cls, object_subparsers) object_subparsers.required = True return parser def get_dict(obj, fields): if isinstance(obj, str): return obj if fields: return {k: v for k, v in obj.attributes.items() if k in fields} return obj.attributes class JSONPrinter(object): def display(self, d, **kwargs): import json # noqa print(json.dumps(d)) def display_list(self, data, fields, **kwargs): import json # noqa print(json.dumps([get_dict(obj, fields) for obj in data])) class YAMLPrinter(object): def display(self, d, **kwargs): try: import yaml # noqa print(yaml.safe_dump(d, default_flow_style=False)) except ImportError: exit( "PyYaml is not installed.\n" "Install it with `pip install PyYaml` " "to use the yaml output feature" ) def display_list(self, data, fields, **kwargs): try: import yaml # noqa print( yaml.safe_dump( [get_dict(obj, fields) for obj in data], default_flow_style=False ) ) except ImportError: exit( "PyYaml is not installed.\n" "Install it with `pip install PyYaml` " "to use the yaml output feature" ) 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) or "None" value = value.replace("\r", "").replace("\n", " ") # If the attribute is a note (ProjectCommitComment) then we do # some modifications to fit everything on one line line = "%s: %s" % (obj._short_print_attr, value) # ellipsize long lines (comments) if len(line) > 79: line = line[:76] + "..." print(line) def display_list(self, data, fields, **kwargs): verbose = kwargs.get("verbose", False) for obj in data: if isinstance(obj, gitlab.base.RESTObject): self.display(get_dict(obj, fields), verbose=verbose, obj=obj) else: print(obj) print("") PRINTERS = {"json": JSONPrinter, "legacy": LegacyPrinter, "yaml": YAMLPrinter} def run(gl, what, action, args, verbose, output, fields): g_cli = GitlabCLI(gl, what, action, args) data = g_cli() printer = PRINTERS[output]() if isinstance(data, dict): printer.display(data, verbose=True, obj=data) elif isinstance(data, list): printer.display_list(data, fields, verbose=verbose) elif isinstance(data, gitlab.base.RESTObject): printer.display(get_dict(data, fields), verbose=verbose, obj=data) elif isinstance(data, str): print(data) elif hasattr(data, "decode"): print(data.decode()) python-gitlab-2.0.1/gitlab/v4/objects.py000066400000000000000000005223111361651701000200750ustar00rootroot00000000000000# -*- 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 types 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 AuditEvent(RESTObject): _id_attr = "id" class AuditEventManager(ListMixin, RESTManager): _path = "/audit_events" _obj_cls = AuditEvent _list_filters = ("created_after", "created_before", "entity_type", "entity_id") class EventManager(ListMixin, RESTManager): _path = "/events" _obj_cls = Event _list_filters = ("action", "target_type", "before", "after", "sort") class UserActivities(RESTObject): _id_attr = "username" class UserStatus(RESTObject): _id_attr = None _short_print_attr = "message" class UserStatusManager(GetWithoutIdMixin, RESTManager): _path = "/users/%(user_id)s/status" _obj_cls = UserStatus _from_parent_attrs = {"user_id": "id"} 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(ListMixin, CreateMixin, DeleteMixin, RESTManager): _path = "/users/%(user_id)s/keys" _obj_cls = UserKey _from_parent_attrs = {"user_id": "id"} _create_attrs = (("title", "key"), tuple()) class UserStatus(RESTObject): pass class UserStatusManager(GetWithoutIdMixin, RESTManager): _path = "/users/%(user_id)s/status" _obj_cls = UserStatus _from_parent_attrs = {"user_id": "id"} 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", "with_custom_attributes", "with_programming_language", "wiki_checksum_failed", "repository_checksum_failed", "min_access_level", "id_after", "id_before", ) 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 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 """ if self._parent: path = "/users/%s/projects" % self._parent.id else: path = "/users/%s/projects" % kwargs["user_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"), ("status", "UserStatusManager"), ) @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 @cli.register_custom_action("User") @exc.on_http_error(exc.GitlabDeactivateError) def deactivate(self, **kwargs): """Deactivate the user. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabDeactivateError: If the user could not be deactivated Returns: bool: Whether the user status has been changed """ path = "/users/%s/deactivate" % self.id server_data = self.manager.gitlab.http_post(path, **kwargs) if server_data: self._attrs["state"] = "deactivated" return server_data @cli.register_custom_action("User") @exc.on_http_error(exc.GitlabActivateError) def activate(self, **kwargs): """Activate the user. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabActivateError: If the user could not be activated Returns: bool: Whether the user status has been changed """ path = "/users/%s/activate" % self.id server_data = self.manager.gitlab.http_post(path, **kwargs) if server_data: 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", "status", "two_factor", ) _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", "avatar", "public_email", "private_profile", ), ) _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", "avatar", "public_email", "private_profile", ), ) _types = {"confirm": types.LowercaseStringAttribute, "avatar": types.ImageAttribute} 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 CurrentUserStatus(SaveMixin, RESTObject): _id_attr = None _short_print_attr = "message" class CurrentUserStatusManager(GetWithoutIdMixin, UpdateMixin, RESTManager): _path = "/user/status" _obj_cls = CurrentUserStatus _update_attrs = (tuple(), ("emoji", "message")) class CurrentUser(RESTObject): _id_attr = None _short_print_attr = "username" _managers = ( ("status", "CurrentUserStatusManager"), ("emails", "CurrentUserEmailManager"), ("gpgkeys", "CurrentUserGPGKeyManager"), ("keys", "CurrentUserKeyManager"), ) class CurrentUserManager(GetWithoutIdMixin, RESTManager): _path = "/user" _obj_cls = CurrentUser class ApplicationAppearance(SaveMixin, RESTObject): _id_attr = None class ApplicationAppearanceManager(GetWithoutIdMixin, UpdateMixin, RESTManager): _path = "/application/appearance" _obj_cls = ApplicationAppearance _update_attrs = ( tuple(), ( "title", "description", "logo", "header_logo", "favicon", "new_project_guidelines", "header_message", "footer_message", "message_background_color", "message_font_color", "email_header_and_footer_enabled", ), ) @exc.on_http_error(exc.GitlabUpdateError) def update(self, id=None, new_data=None, **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 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 """ new_data = new_data or {} data = new_data.copy() super(ApplicationAppearanceManager, self).update(id, data, **kwargs) class ApplicationSettings(SaveMixin, RESTObject): _id_attr = None class ApplicationSettingsManager(GetWithoutIdMixin, UpdateMixin, RESTManager): _path = "/application/settings" _obj_cls = ApplicationSettings _update_attrs = ( tuple(), ( "id", "default_projects_limit", "signup_enabled", "password_authentication_enabled_for_web", "gravatar_enabled", "sign_in_text", "created_at", "updated_at", "home_page_url", "default_branch_protection", "restricted_visibility_levels", "max_attachment_size", "session_expire_delay", "default_project_visibility", "default_snippet_visibility", "default_group_visibility", "outbound_local_requests_whitelist", "domain_whitelist", "domain_blacklist_enabled", "domain_blacklist", "external_authorization_service_enabled", "external_authorization_service_url", "external_authorization_service_default_label", "external_authorization_service_timeout", "user_oauth_applications", "after_sign_out_path", "container_registry_token_expire_delay", "repository_storages", "plantuml_enabled", "plantuml_url", "terminal_max_session_time", "polling_interval_multiplier", "rsa_key_restriction", "dsa_key_restriction", "ecdsa_key_restriction", "ed25519_key_restriction", "first_day_of_week", "enforce_terms", "terms", "performance_bar_allowed_group_id", "instance_statistics_visibility_private", "user_show_add_ssh_key_message", "file_template_project_id", "local_markdown_version", "asset_proxy_enabled", "asset_proxy_url", "asset_proxy_whitelist", "geo_node_allowed_ips", "allow_local_requests_from_hooks_and_services", "allow_local_requests_from_web_hooks_and_services", "allow_local_requests_from_system_hooks", ), ) @exc.on_http_error(exc.GitlabUpdateError) def update(self, id=None, new_data=None, **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 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 """ new_data = new_data or {} data = new_data.copy() if "domain_whitelist" in data and data["domain_whitelist"] is None: data.pop("domain_whitelist") super(ApplicationSettingsManager, self).update(id, data, **kwargs) 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(ListMixin, 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(ObjectDeleteMixin, RESTObject): _id_attr = "name" class FeatureManager(ListMixin, DeleteMixin, 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(ListMixin, CreateMixin, DeleteMixin, RESTManager): _path = "/groups/%(group_id)s/access_requests" _obj_cls = GroupAccessRequest _from_parent_attrs = {"group_id": "id"} class GroupBadge(SaveMixin, ObjectDeleteMixin, RESTObject): pass class GroupBadgeManager(BadgeRenderMixin, CRUDMixin, RESTManager): _path = "/groups/%(group_id)s/badges" _obj_cls = GroupBadge _from_parent_attrs = {"group_id": "id"} _create_attrs = (("link_url", "image_url"), tuple()) _update_attrs = (tuple(), ("link_url", "image_url")) class GroupBoardList(SaveMixin, ObjectDeleteMixin, RESTObject): pass class GroupBoardListManager(CRUDMixin, RESTManager): _path = "/groups/%(group_id)s/boards/%(board_id)s/lists" _obj_cls = GroupBoardList _from_parent_attrs = {"group_id": "group_id", "board_id": "id"} _create_attrs = (("label_id",), tuple()) _update_attrs = (("position",), tuple()) class GroupBoard(SaveMixin, ObjectDeleteMixin, RESTObject): _managers = (("lists", "GroupBoardListManager"),) class GroupBoardManager(CRUDMixin, RESTManager): _path = "/groups/%(group_id)s/boards" _obj_cls = GroupBoard _from_parent_attrs = {"group_id": "id"} _create_attrs = (("name",), tuple()) class GroupCluster(SaveMixin, ObjectDeleteMixin, RESTObject): pass class GroupClusterManager(CRUDMixin, RESTManager): _path = "/groups/%(group_id)s/clusters" _obj_cls = GroupCluster _from_parent_attrs = {"group_id": "id"} _create_attrs = ( ("name", "platform_kubernetes_attributes"), ("domain", "enabled", "managed", "environment_scope"), ) _update_attrs = ( tuple(), ( "name", "domain", "management_project_id", "platform_kubernetes_attributes", "environment_scope", ), ) @exc.on_http_error(exc.GitlabStopError) 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 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 = "%s/user" % (self.path) return CreateMixin.create(self, data, path=path, **kwargs) 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 GroupEpicIssue(ObjectDeleteMixin, SaveMixin, RESTObject): _id_attr = "epic_issue_id" 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() self.manager.update(obj_id, updated_data, **kwargs) class GroupEpicIssueManager( ListMixin, CreateMixin, UpdateMixin, DeleteMixin, RESTManager ): _path = "/groups/%(group_id)s/epics/%(epic_iid)s/issues" _obj_cls = GroupEpicIssue _from_parent_attrs = {"group_id": "group_id", "epic_iid": "iid"} _create_attrs = (("issue_id",), tuple()) _update_attrs = (tuple(), ("move_before_id", "move_after_id")) @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 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 manage object class build with the data sent by the server """ CreateMixin._check_missing_create_attrs(self, data) path = "%s/%s" % (self.path, data.pop("issue_id")) server_data = self.gitlab.http_post(path, **kwargs) # The epic_issue_id attribute doesn't exist when creating the resource, # but is used everywhere elese. Let's create it to be consistent client # side server_data["epic_issue_id"] = server_data["id"] return self._obj_cls(self, server_data) class GroupEpicResourceLabelEvent(RESTObject): pass class GroupEpicResourceLabelEventManager(RetrieveMixin, RESTManager): _path = "/groups/%(group_id)s/epics/%(epic_id)s/resource_label_events" _obj_cls = GroupEpicResourceLabelEvent _from_parent_attrs = {"group_id": "group_id", "epic_id": "id"} class GroupEpic(ObjectDeleteMixin, SaveMixin, RESTObject): _id_attr = "iid" _managers = ( ("issues", "GroupEpicIssueManager"), ("resourcelabelevents", "GroupEpicResourceLabelEventManager"), ) class GroupEpicManager(CRUDMixin, RESTManager): _path = "/groups/%(group_id)s/epics" _obj_cls = GroupEpic _from_parent_attrs = {"group_id": "id"} _list_filters = ("author_id", "labels", "order_by", "sort", "search") _create_attrs = (("title",), ("labels", "description", "start_date", "end_date")) _update_attrs = ( tuple(), ("title", "labels", "description", "start_date", "end_date"), ) _types = {"labels": types.ListAttribute} class GroupIssue(RESTObject): pass class GroupIssueManager(ListMixin, RESTManager): _path = "/groups/%(group_id)s/issues" _obj_cls = GroupIssue _from_parent_attrs = {"group_id": "id"} _list_filters = ( "state", "labels", "milestone", "order_by", "sort", "iids", "author_id", "assignee_id", "my_reaction_emoji", "search", "created_after", "created_before", "updated_after", "updated_before", ) _types = {"labels": types.ListAttribute} class GroupLabel(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 GroupLabelManager(ListMixin, CreateMixin, UpdateMixin, DeleteMixin, RESTManager): _path = "/groups/%(group_id)s/labels" _obj_cls = GroupLabel _from_parent_attrs = {"group_id": "id"} _create_attrs = (("name", "color"), ("description", "priority")) _update_attrs = (("name",), ("new_name", "color", "description", "priority")) # Update without ID. def update(self, name, new_data=None, **kwargs): """Update a Label on the server. Args: name: The name of the label **kwargs: Extra options to send to the server (e.g. sudo) """ new_data = new_data or {} if name: new_data["name"] = name return super().update(id=None, new_data=new_data, **kwargs) # 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 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 GroupMember(SaveMixin, ObjectDeleteMixin, RESTObject): _short_print_attr = "username" class GroupMemberManager(CRUDMixin, 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",)) @cli.register_custom_action("GroupMemberManager") @exc.on_http_error(exc.GitlabListError) def all(self, **kwargs): """List all the members, included inherited ones. 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 members """ path = "%s/all" % self.path obj = self.gitlab.http_list(path, **kwargs) return [self._obj_cls(self, item) for item in obj] class GroupMergeRequest(RESTObject): pass class GroupMergeRequestManager(ListMixin, RESTManager): _path = "/groups/%(group_id)s/merge_requests" _obj_cls = GroupMergeRequest _from_parent_attrs = {"group_id": "id"} _list_filters = ( "state", "order_by", "sort", "milestone", "view", "labels", "created_after", "created_before", "updated_after", "updated_before", "scope", "author_id", "assignee_id", "my_reaction_emoji", "source_branch", "target_branch", "search", ) _types = {"labels": types.ListAttribute} 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(ListMixin, RESTManager): _path = "/groups/%(group_id)s/projects" _obj_cls = GroupProject _from_parent_attrs = {"group_id": "id"} _list_filters = ( "archived", "visibility", "order_by", "sort", "search", "simple", "owned", "starred", "with_custom_attributes", "include_subgroups", "with_issues_enabled", "with_merge_requests_enabled", "with_shared", "min_access_level", "with_security_reports", ) class GroupSubgroup(RESTObject): pass class GroupSubgroupManager(ListMixin, 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", "with_custom_attributes", ) 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", "variable_type")) _update_attrs = (("key", "value"), ("protected", "variable_type")) class Group(SaveMixin, ObjectDeleteMixin, RESTObject): _short_print_attr = "name" _managers = ( ("accessrequests", "GroupAccessRequestManager"), ("badges", "GroupBadgeManager"), ("boards", "GroupBoardManager"), ("customattributes", "GroupCustomAttributeManager"), ("epics", "GroupEpicManager"), ("issues", "GroupIssueManager"), ("labels", "GroupLabelManager"), ("members", "GroupMemberManager"), ("mergerequests", "GroupMergeRequestManager"), ("milestones", "GroupMilestoneManager"), ("notificationsettings", "GroupNotificationSettingsManager"), ("projects", "GroupProjectManager"), ("subgroups", "GroupSubgroupManager"), ("variables", "GroupVariableManager"), ("clusters", "GroupClusterManager"), ) @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/%s/projects/%s" % (self.id, to_project_id) self.manager.gitlab.http_post(path, **kwargs) @cli.register_custom_action("Group", ("scope", "search")) @exc.on_http_error(exc.GitlabSearchError) def search(self, scope, search, **kwargs): """Search the group resources matching the provided string.' Args: scope (str): Scope of the search search (str): Search string **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabSearchError: If the server failed to perform the request Returns: GitlabList: A list of dicts describing the resources found. """ data = {"scope": scope, "search": search} path = "/groups/%s/search" % self.get_id() return self.manager.gitlab.http_list(path, query_data=data, **kwargs) @cli.register_custom_action("Group", ("cn", "group_access", "provider")) @exc.on_http_error(exc.GitlabCreateError) def add_ldap_group_link(self, cn, group_access, provider, **kwargs): """Add an LDAP group link. Args: cn (str): CN of the LDAP group group_access (int): Minimum access level for members of the LDAP group provider (str): LDAP provider for the LDAP group **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 """ path = "/groups/%s/ldap_group_links" % self.get_id() data = {"cn": cn, "group_access": group_access, "provider": provider} self.manager.gitlab.http_post(path, post_data=data, **kwargs) @cli.register_custom_action("Group", ("cn",), ("provider",)) @exc.on_http_error(exc.GitlabDeleteError) def delete_ldap_group_link(self, cn, provider=None, **kwargs): """Delete an LDAP group link. Args: cn (str): CN of the LDAP group provider (str): LDAP provider for the LDAP group **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 """ path = "/groups/%s/ldap_group_links" % self.get_id() if provider is not None: path += "/%s" % provider path += "/%s" % cn self.manager.gitlab.http_delete(path) @cli.register_custom_action("Group") @exc.on_http_error(exc.GitlabCreateError) def ldap_sync(self, **kwargs): """Sync LDAP groups. Args: **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 """ path = "/groups/%s/ldap_sync" % self.get_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", "with_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(ListMixin, RESTManager): _path = "/issues" _obj_cls = Issue _list_filters = ( "state", "labels", "milestone", "scope", "author_id", "assignee_id", "my_reaction_emoji", "iids", "order_by", "sort", "search", "created_after", "created_before", "updated_after", "updated_before", ) _types = {"labels": types.ListAttribute} class LDAPGroup(RESTObject): _id_attr = None class LDAPGroupManager(RESTManager): _path = "/ldap/groups" _obj_cls = LDAPGroup _list_filters = ("search", "provider") @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 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 """ data = kwargs.copy() if self.gitlab.per_page: data.setdefault("per_page", self.gitlab.per_page) if "provider" in data: path = "/ldap/%s/groups" % data["provider"] else: path = self._path obj = self.gitlab.http_list(path, **data) if isinstance(obj, list): return [self._obj_cls(self, item) for item in obj] else: return base.RESTObjectList(self, self._obj_cls, obj) 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 MergeRequest(RESTObject): pass class MergeRequestManager(ListMixin, RESTManager): _path = "/merge_requests" _obj_cls = MergeRequest _from_parent_attrs = {"group_id": "id"} _list_filters = ( "state", "order_by", "sort", "milestone", "view", "labels", "created_after", "created_before", "updated_after", "updated_before", "scope", "author_id", "assignee_id", "my_reaction_emoji", "source_branch", "target_branch", "search", ) _types = {"labels": types.ListAttribute} class Snippet(UserAgentDetailMixin, 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, raw=True, **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(RetrieveMixin, 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 ProjectRegistryRepository(ObjectDeleteMixin, RESTObject): _managers = (("tags", "ProjectRegistryTagManager"),) class ProjectRegistryRepositoryManager(DeleteMixin, ListMixin, RESTManager): _path = "/projects/%(project_id)s/registry/repositories" _obj_cls = ProjectRegistryRepository _from_parent_attrs = {"project_id": "id"} class ProjectRegistryTag(ObjectDeleteMixin, RESTObject): _id_attr = "name" class ProjectRegistryTagManager(DeleteMixin, RetrieveMixin, RESTManager): _obj_cls = ProjectRegistryTag _from_parent_attrs = {"project_id": "project_id", "repository_id": "id"} _path = "/projects/%(project_id)s/registry/repositories/%(repository_id)s/tags" @cli.register_custom_action( "ProjectRegistryTagManager", optional=("name_regex", "keep_n", "older_than") ) @exc.on_http_error(exc.GitlabDeleteError) def delete_in_bulk(self, name_regex=".*", **kwargs): """Delete Tag in bulk Args: name_regex (string): The regex of the name to delete. To delete all tags specify .*. keep_n (integer): The amount of latest tags of given name to keep. older_than (string): Tags to delete that are older than the given time, written in human readable form 1h, 1d, 1month. **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 """ valid_attrs = ["keep_n", "older_than"] data = {"name_regex": name_regex} data.update({k: v for k, v in kwargs.items() if k in valid_attrs}) self.gitlab.http_delete(self.path, query_data=data, **kwargs) 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(SaveMixin, ObjectDeleteMixin, RESTObject): _managers = (("lists", "ProjectBoardListManager"),) class ProjectBoardManager(CRUDMixin, RESTManager): _path = "/projects/%(project_id)s/boards" _obj_cls = ProjectBoard _from_parent_attrs = {"project_id": "id"} _create_attrs = (("name",), tuple()) 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 """ id = self.get_id().replace("/", "%2F") path = "%s/%s/protect" % (self.manager.path, 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 """ id = self.get_id().replace("/", "%2F") path = "%s/%s/unprotect" % (self.manager.path, 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 ProjectCluster(SaveMixin, ObjectDeleteMixin, RESTObject): pass class ProjectClusterManager(CRUDMixin, RESTManager): _path = "/projects/%(project_id)s/clusters" _obj_cls = ProjectCluster _from_parent_attrs = {"project_id": "id"} _create_attrs = ( ("name", "platform_kubernetes_attributes"), ("domain", "enabled", "managed", "environment_scope"), ) _update_attrs = ( tuple(), ( "name", "domain", "management_project_id", "platform_kubernetes_attributes", "environment_scope", ), ) @exc.on_http_error(exc.GitlabStopError) 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 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 = "%s/user" % (self.path) return CreateMixin.create(self, data, path=path, **kwargs) 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, RefreshMixin): @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.GitlabCreateError) def delete_artifacts(self, **kwargs): """Delete artifacts of a job. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabDeleteError: If the request could not be performed """ path = "%s/%s/artifacts" % (self.manager.path, self.get_id()) self.manager.gitlab.http_delete(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, raw=True, **kwargs ) return utils.response_content(result, streamed, action, chunk_size) @cli.register_custom_action("ProjectJob") @exc.on_http_error(exc.GitlabGetError) def artifact(self, path, streamed=False, action=None, chunk_size=1024, **kwargs): """Get a single artifact file from within the job's artifacts archive. Args: path (str): Path of the artifact 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/%s" % (self.manager.path, self.get_id(), path) result = self.manager.gitlab.http_get( path, streamed=streamed, raw=True, **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, raw=True, **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, RefreshMixin): pass class ProjectCommitStatusManager(ListMixin, 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"), ) @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 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 """ # project_id and commit_id are in the data dict when using the CLI, but # they are missing when using only the API # See #511 base_path = "/projects/%(project_id)s/statuses/%(commit_id)s" if "project_id" in data and "commit_id" in data: path = base_path % data else: path = self._compute_path(base_path) return CreateMixin.create(self, data, path=path, **kwargs) class ProjectCommitComment(RESTObject): _id_attr = None _short_print_attr = "note" 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 ProjectCommitDiscussionNote(SaveMixin, ObjectDeleteMixin, RESTObject): pass class ProjectCommitDiscussionNoteManager( GetMixin, CreateMixin, UpdateMixin, DeleteMixin, RESTManager ): _path = ( "/projects/%(project_id)s/repository/commits/%(commit_id)s/" "discussions/%(discussion_id)s/notes" ) _obj_cls = ProjectCommitDiscussionNote _from_parent_attrs = { "project_id": "project_id", "commit_id": "commit_id", "discussion_id": "id", } _create_attrs = (("body",), ("created_at", "position")) _update_attrs = (("body",), tuple()) class ProjectCommitDiscussion(RESTObject): _managers = (("notes", "ProjectCommitDiscussionNoteManager"),) class ProjectCommitDiscussionManager(RetrieveMixin, CreateMixin, RESTManager): _path = "/projects/%(project_id)s/repository/commits/%(commit_id)s/" "discussions" _obj_cls = ProjectCommitDiscussion _from_parent_attrs = {"project_id": "project_id", "commit_id": "id"} _create_attrs = (("body",), ("created_at",)) class ProjectCommit(RESTObject): _short_print_attr = "title" _managers = ( ("comments", "ProjectCommitCommentManager"), ("discussions", "ProjectCommitDiscussionManager"), ("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) @cli.register_custom_action("ProjectCommit", optional=("type",)) @exc.on_http_error(exc.GitlabGetError) def refs(self, type="all", **kwargs): """List the references the commit is pushed to. Args: type (str): The scope of references ('branch', 'tag' or 'all') **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabGetError: If the references could not be retrieved Returns: list: The references the commit is pushed to. """ path = "%s/%s/refs" % (self.manager.path, self.get_id()) data = {"type": type} return self.manager.gitlab.http_get(path, query_data=data, **kwargs) @cli.register_custom_action("ProjectCommit") @exc.on_http_error(exc.GitlabGetError) def merge_requests(self, **kwargs): """List the merge requests related to the commit. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabGetError: If the references could not be retrieved Returns: list: The merge requests related to the commit. """ path = "%s/%s/merge_requests" % (self.manager.path, self.get_id()) return self.manager.gitlab.http_get(path, **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): @cli.register_custom_action("ProjectEnvironment") @exc.on_http_error(exc.GitlabStopError) def stop(self, **kwargs): """Stop the environment. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabStopError: If the operation failed """ path = "%s/%s/stop" % (self.manager.path, self.get_id()) self.manager.gitlab.http_post(path, **kwargs) class ProjectEnvironmentManager( RetrieveMixin, 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(SaveMixin, ObjectDeleteMixin, RESTObject): pass class ProjectKeyManager(CRUDMixin, RESTManager): _path = "/projects/%(project_id)s/deploy_keys" _obj_cls = ProjectKey _from_parent_attrs = {"project_id": "id"} _create_attrs = (("title", "key"), ("can_push",)) _update_attrs = (tuple(), ("title", "can_push")) @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 ProjectBadge(SaveMixin, ObjectDeleteMixin, RESTObject): pass class ProjectBadgeManager(BadgeRenderMixin, CRUDMixin, RESTManager): _path = "/projects/%(project_id)s/badges" _obj_cls = ProjectBadge _from_parent_attrs = {"project_id": "id"} _create_attrs = (("link_url", "image_url"), tuple()) _update_attrs = (tuple(), ("link_url", "image_url")) 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, ListMixin, RESTManager): _path = "/projects/%(project_id)s/forks" _obj_cls = ProjectFork _from_parent_attrs = {"project_id": "id"} _list_filters = ( "archived", "visibility", "order_by", "sort", "search", "simple", "owned", "membership", "starred", "statistics", "with_custom_attributes", "with_issues_enabled", "with_merge_requests_enabled", ) _create_attrs = (tuple(), ("namespace",)) 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 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", "confidential_issues_events", "merge_requests_events", "tag_push_events", "note_events", "job_events", "pipeline_events", "wiki_page_events", "enable_ssl_verification", "token", ), ) _update_attrs = ( ("url",), ( "push_events", "issues_events", "confidential_issues_events", "merge_requests_events", "tag_push_events", "note_events", "job_events", "pipeline_events", "wiki_events", "enable_ssl_verification", "token", ), ) 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 ProjectIssueDiscussionNote(SaveMixin, ObjectDeleteMixin, RESTObject): pass class ProjectIssueDiscussionNoteManager( GetMixin, CreateMixin, UpdateMixin, DeleteMixin, RESTManager ): _path = ( "/projects/%(project_id)s/issues/%(issue_iid)s/" "discussions/%(discussion_id)s/notes" ) _obj_cls = ProjectIssueDiscussionNote _from_parent_attrs = { "project_id": "project_id", "issue_iid": "issue_iid", "discussion_id": "id", } _create_attrs = (("body",), ("created_at",)) _update_attrs = (("body",), tuple()) class ProjectIssueDiscussion(RESTObject): _managers = (("notes", "ProjectIssueDiscussionNoteManager"),) class ProjectIssueDiscussionManager(RetrieveMixin, CreateMixin, RESTManager): _path = "/projects/%(project_id)s/issues/%(issue_iid)s/discussions" _obj_cls = ProjectIssueDiscussion _from_parent_attrs = {"project_id": "project_id", "issue_iid": "iid"} _create_attrs = (("body",), ("created_at",)) class ProjectIssueLink(ObjectDeleteMixin, RESTObject): _id_attr = "issue_link_id" class ProjectIssueLinkManager(ListMixin, CreateMixin, DeleteMixin, RESTManager): _path = "/projects/%(project_id)s/issues/%(issue_iid)s/links" _obj_cls = ProjectIssueLink _from_parent_attrs = {"project_id": "project_id", "issue_iid": "iid"} _create_attrs = (("target_project_id", "target_issue_iid"), 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 server (e.g. sudo) Returns: RESTObject, RESTObject: The source and target issues Raises: GitlabAuthenticationError: If authentication is not correct GitlabCreateError: If the server cannot perform the request """ self._check_missing_create_attrs(data) server_data = self.gitlab.http_post(self.path, post_data=data, **kwargs) source_issue = ProjectIssue(self._parent.manager, server_data["source_issue"]) target_issue = ProjectIssue(self._parent.manager, server_data["target_issue"]) return source_issue, target_issue class ProjectIssueResourceLabelEvent(RESTObject): pass class ProjectIssueResourceLabelEventManager(RetrieveMixin, RESTManager): _path = "/projects/%(project_id)s/issues/%(issue_iid)s" "/resource_label_events" _obj_cls = ProjectIssueResourceLabelEvent _from_parent_attrs = {"project_id": "project_id", "issue_iid": "iid"} class ProjectIssue( UserAgentDetailMixin, SubscribableMixin, TodoMixin, TimeTrackingMixin, ParticipantsMixin, SaveMixin, ObjectDeleteMixin, RESTObject, ): _short_print_attr = "title" _id_attr = "iid" _managers = ( ("awardemojis", "ProjectIssueAwardEmojiManager"), ("discussions", "ProjectIssueDiscussionManager"), ("links", "ProjectIssueLinkManager"), ("notes", "ProjectIssueNoteManager"), ("resourcelabelevents", "ProjectIssueResourceLabelEventManager"), ) @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) @cli.register_custom_action("ProjectIssue") @exc.on_http_error(exc.GitlabGetError) def related_merge_requests(self, **kwargs): """List merge requests related to the issue. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabGetErrot: If the merge requests could not be retrieved Returns: list: The list of merge requests. """ path = "%s/%s/related_merge_requests" % (self.manager.path, self.get_id()) return self.manager.gitlab.http_get(path, **kwargs) @cli.register_custom_action("ProjectIssue") @exc.on_http_error(exc.GitlabGetError) def closed_by(self, **kwargs): """List merge requests that will close the issue when merged. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabGetErrot: If the merge requests could not be retrieved Returns: list: The list of merge requests. """ path = "%s/%s/closed_by" % (self.manager.path, self.get_id()) return self.manager.gitlab.http_get(path, **kwargs) class ProjectIssueManager(CRUDMixin, RESTManager): _path = "/projects/%(project_id)s/issues" _obj_cls = ProjectIssue _from_parent_attrs = {"project_id": "id"} _list_filters = ( "iids", "state", "labels", "milestone", "scope", "author_id", "assignee_id", "my_reaction_emoji", "order_by", "sort", "search", "created_after", "created_before", "updated_after", "updated_before", ) _create_attrs = ( ("title",), ( "description", "confidential", "assignee_ids", "assignee_id", "milestone_id", "labels", "created_at", "due_date", "merge_request_to_resolve_discussions_of", "discussion_to_resolve", ), ) _update_attrs = ( tuple(), ( "title", "description", "confidential", "assignee_ids", "assignee_id", "milestone_id", "labels", "state_event", "updated_at", "due_date", "discussion_locked", ), ) _types = {"labels": types.ListAttribute} 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",)) @cli.register_custom_action("ProjectMemberManager") @exc.on_http_error(exc.GitlabListError) def all(self, **kwargs): """List all the members, included inherited ones. 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 members """ path = "%s/all" % self.path obj = self.gitlab.http_list(path, **kwargs) return [self._obj_cls(self, item) for item in obj] 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 ProjectRelease(RESTObject): _id_attr = "tag_name" class ProjectReleaseManager(NoUpdateMixin, RESTManager): _path = "/projects/%(project_id)s/releases" _obj_cls = ProjectRelease _from_parent_attrs = {"project_id": "id"} _create_attrs = (("name", "tag_name", "description"), ("ref", "assets")) 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 ProjectProtectedTag(ObjectDeleteMixin, RESTObject): _id_attr = "name" _short_print_attr = "name" class ProjectProtectedTagManager(NoUpdateMixin, RESTManager): _path = "/projects/%(project_id)s/protected_tags" _obj_cls = ProjectProtectedTag _from_parent_attrs = {"project_id": "id"} _create_attrs = (("name",), ("create_access_level",)) class ProjectMergeRequestApproval(SaveMixin, RESTObject): _id_attr = None class ProjectMergeRequestApprovalManager(GetWithoutIdMixin, UpdateMixin, RESTManager): _path = "/projects/%(project_id)s/merge_requests/%(mr_iid)s/approvals" _obj_cls = ProjectMergeRequestApproval _from_parent_attrs = {"project_id": "project_id", "mr_iid": "iid"} _update_attrs = (("approvals_required",), tuple()) _update_uses_post = True @exc.on_http_error(exc.GitlabUpdateError) def set_approvers(self, approver_ids=None, approver_group_ids=None, **kwargs): """Change MR-level allowed approvers and approver groups. Args: approver_ids (list): User IDs that can approve MRs approver_group_ids (list): Group IDs whose members can approve MRs Raises: GitlabAuthenticationError: If authentication is not correct GitlabUpdateError: If the server failed to perform the request """ approver_ids = approver_ids or [] approver_group_ids = approver_group_ids or [] path = "%s/%s/approvers" % (self._parent.manager.path, self._parent.get_id()) data = {"approver_ids": approver_ids, "approver_group_ids": approver_group_ids} self.gitlab.http_put(path, post_data=data, **kwargs) 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": "mr_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 ProjectMergeRequestDiscussionNote(SaveMixin, ObjectDeleteMixin, RESTObject): pass class ProjectMergeRequestDiscussionNoteManager( GetMixin, CreateMixin, UpdateMixin, DeleteMixin, RESTManager ): _path = ( "/projects/%(project_id)s/merge_requests/%(mr_iid)s/" "discussions/%(discussion_id)s/notes" ) _obj_cls = ProjectMergeRequestDiscussionNote _from_parent_attrs = { "project_id": "project_id", "mr_iid": "mr_iid", "discussion_id": "id", } _create_attrs = (("body",), ("created_at",)) _update_attrs = (("body",), tuple()) class ProjectMergeRequestDiscussion(SaveMixin, RESTObject): _managers = (("notes", "ProjectMergeRequestDiscussionNoteManager"),) class ProjectMergeRequestDiscussionManager( RetrieveMixin, CreateMixin, UpdateMixin, RESTManager ): _path = "/projects/%(project_id)s/merge_requests/%(mr_iid)s/discussions" _obj_cls = ProjectMergeRequestDiscussion _from_parent_attrs = {"project_id": "project_id", "mr_iid": "iid"} _create_attrs = (("body",), ("created_at", "position")) _update_attrs = (("resolved",), tuple()) class ProjectMergeRequestResourceLabelEvent(RESTObject): pass class ProjectMergeRequestResourceLabelEventManager(RetrieveMixin, RESTManager): _path = ( "/projects/%(project_id)s/merge_requests/%(mr_iid)s" "/resource_label_events" ) _obj_cls = ProjectMergeRequestResourceLabelEvent _from_parent_attrs = {"project_id": "project_id", "mr_iid": "iid"} class ProjectMergeRequest( SubscribableMixin, TodoMixin, TimeTrackingMixin, ParticipantsMixin, SaveMixin, ObjectDeleteMixin, RESTObject, ): _id_attr = "iid" _managers = ( ("approvals", "ProjectMergeRequestApprovalManager"), ("awardemojis", "ProjectMergeRequestAwardEmojiManager"), ("diffs", "ProjectMergeRequestDiffManager"), ("discussions", "ProjectMergeRequestDiscussionManager"), ("notes", "ProjectMergeRequestNoteManager"), ("resourcelabelevents", "ProjectMergeRequestResourceLabelEventManager"), ) @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") @exc.on_http_error(exc.GitlabListError) def pipelines(self, **kwargs): """List the merge request pipelines. 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/pipelines" % (self.manager.path, self.get_id()) return self.manager.gitlab.http_get(path, **kwargs) @cli.register_custom_action("ProjectMergeRequest", tuple(), ("sha")) @exc.on_http_error(exc.GitlabMRApprovalError) def approve(self, sha=None, **kwargs): """Approve the merge request. Args: sha (str): Head SHA of MR **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabMRApprovalError: If the approval failed """ path = "%s/%s/approve" % (self.manager.path, self.get_id()) data = {} if sha: data["sha"] = sha server_data = self.manager.gitlab.http_post(path, post_data=data, **kwargs) self._update_attrs(server_data) @cli.register_custom_action("ProjectMergeRequest") @exc.on_http_error(exc.GitlabMRApprovalError) def unapprove(self, **kwargs): """Unapprove the merge request. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabMRApprovalError: If the unapproval failed """ path = "%s/%s/unapprove" % (self.manager.path, self.get_id()) data = {} server_data = self.manager.gitlab.http_post(path, post_data=data, **kwargs) self._update_attrs(server_data) @cli.register_custom_action("ProjectMergeRequest") @exc.on_http_error(exc.GitlabMRRebaseError) def rebase(self, **kwargs): """Attempt to rebase the source branch onto the target branch Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabMRRebaseError: If rebasing failed """ path = "%s/%s/rebase" % (self.manager.path, self.get_id()) data = {} return self.manager.gitlab.http_put(path, post_data=data, **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) 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", "allow_maintainer_to_push", "squash", ), ) _update_attrs = ( tuple(), ( "target_branch", "assignee_id", "title", "description", "state_event", "labels", "milestone_id", "remove_source_branch", "discussion_locked", "allow_maintainer_to_push", "squash", ), ) _list_filters = ( "state", "order_by", "sort", "milestone", "view", "labels", "created_after", "created_before", "updated_after", "updated_before", "scope", "author_id", "assignee_id", "my_reaction_emoji", "source_branch", "target_branch", "search", ) _types = {"labels": types.ListAttribute} 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( ListMixin, 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")) # Update without ID. def update(self, name, new_data=None, **kwargs): """Update a Label on the server. Args: name: The name of the label **kwargs: Extra options to send to the server (e.g. sudo) """ new_data = new_data or {} if name: new_data["name"] = name return super().update(id=None, new_data=new_data, **kwargs) # 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 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 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 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=None, **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 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 """ new_data = new_data or {} 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 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 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, raw=True, **kwargs ) return utils.response_content(result, streamed, action, chunk_size) @cli.register_custom_action("ProjectFileManager", ("file_path", "ref")) @exc.on_http_error(exc.GitlabListError) def blame(self, file_path, ref, **kwargs): """Return the content of a file for a commit. 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 server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabListError: If the server failed to perform the request Returns: list(blame): a list of commits/lines matching the file """ file_path = file_path.replace("/", "%2F").replace(".", "%2E") path = "%s/%s/blame" % (self.path, file_path) query_data = {"ref": ref} return self.gitlab.http_list(path, query_data, **kwargs) class ProjectPipelineJob(RESTObject): pass class ProjectPipelineJobManager(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 ProjectPipelineVariable(RESTObject): _id_attr = "key" class ProjectPipelineVariableManager(ListMixin, RESTManager): _path = "/projects/%(project_id)s/pipelines/%(pipeline_id)s/variables" _obj_cls = ProjectPipelineVariable _from_parent_attrs = {"project_id": "project_id", "pipeline_id": "id"} class ProjectPipeline(RESTObject, RefreshMixin, ObjectDeleteMixin): _managers = ( ("jobs", "ProjectPipelineJobManager"), ("variables", "ProjectPipelineVariableManager"), ) @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, DeleteMixin, RESTManager): _path = "/projects/%(project_id)s/pipelines" _obj_cls = ProjectPipeline _from_parent_attrs = {"project_id": "id"} _list_filters = ( "scope", "status", "ref", "sha", "yaml_errors", "name", "username", "order_by", "sort", ) _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 ProjectPushRules(SaveMixin, ObjectDeleteMixin, RESTObject): _id_attr = None class ProjectPushRulesManager( GetWithoutIdMixin, CreateMixin, UpdateMixin, DeleteMixin, RESTManager ): _path = "/projects/%(project_id)s/push_rule" _obj_cls = ProjectPushRules _from_parent_attrs = {"project_id": "id"} _create_attrs = ( tuple(), ( "deny_delete_tag", "member_check", "prevent_secrets", "commit_message_regex", "branch_name_regex", "author_email_regex", "file_name_regex", "max_file_size", ), ) _update_attrs = ( tuple(), ( "deny_delete_tag", "member_check", "prevent_secrets", "commit_message_regex", "branch_name_regex", "author_email_regex", "file_name_regex", "max_file_size", ), ) 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 ProjectSnippetDiscussionNote(SaveMixin, ObjectDeleteMixin, RESTObject): pass class ProjectSnippetDiscussionNoteManager( GetMixin, CreateMixin, UpdateMixin, DeleteMixin, RESTManager ): _path = ( "/projects/%(project_id)s/snippets/%(snippet_id)s/" "discussions/%(discussion_id)s/notes" ) _obj_cls = ProjectSnippetDiscussionNote _from_parent_attrs = { "project_id": "project_id", "snippet_id": "snippet_id", "discussion_id": "id", } _create_attrs = (("body",), ("created_at",)) _update_attrs = (("body",), tuple()) class ProjectSnippetDiscussion(RESTObject): _managers = (("notes", "ProjectSnippetDiscussionNoteManager"),) class ProjectSnippetDiscussionManager(RetrieveMixin, CreateMixin, RESTManager): _path = "/projects/%(project_id)s/snippets/%(snippet_id)s/discussions" _obj_cls = ProjectSnippetDiscussion _from_parent_attrs = {"project_id": "project_id", "snippet_id": "id"} _create_attrs = (("body",), ("created_at",)) class ProjectSnippet(UserAgentDetailMixin, SaveMixin, ObjectDeleteMixin, RESTObject): _url = "/projects/%(project_id)s/snippets" _short_print_attr = "title" _managers = ( ("awardemojis", "ProjectSnippetAwardEmojiManager"), ("discussions", "ProjectSnippetDiscussionManager"), ("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, raw=True, **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", "content", "visibility"), ("description",)) _update_attrs = ( tuple(), ("title", "file_name", "content", "visibility", "description"), ) 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"), ("protected", "variable_type")) _update_attrs = (("key", "value"), ("protected", "variable_type")) 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 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=None, **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 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 """ new_data = new_data or {} 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(ListMixin, CreateMixin, DeleteMixin, RESTManager): _path = "/projects/%(project_id)s/access_requests" _obj_cls = ProjectAccessRequest _from_parent_attrs = {"project_id": "id"} class ProjectApproval(SaveMixin, RESTObject): _id_attr = None class ProjectApprovalManager(GetWithoutIdMixin, UpdateMixin, RESTManager): _path = "/projects/%(project_id)s/approvals" _obj_cls = ProjectApproval _from_parent_attrs = {"project_id": "id"} _update_attrs = ( tuple(), ( "approvals_before_merge", "reset_approvals_on_push", "disable_overriding_approvers_per_merge_request", "merge_requests_author_approval", "merge_requests_disable_committers_approval", ), ) _update_uses_post = True @exc.on_http_error(exc.GitlabUpdateError) def set_approvers(self, approver_ids=None, approver_group_ids=None, **kwargs): """Change project-level allowed approvers and approver groups. Args: approver_ids (list): User IDs that can approve MRs approver_group_ids (list): Group IDs whose members can approve MRs Raises: GitlabAuthenticationError: If authentication is not correct GitlabUpdateError: If the server failed to perform the request """ approver_ids = approver_ids or [] approver_group_ids = approver_group_ids or [] path = "/projects/%s/approvers" % self._parent.get_id() data = {"approver_ids": approver_ids, "approver_group_ids": approver_group_ids} self.gitlab.http_put(path, post_data=data, **kwargs) class ProjectApprovalRule(SaveMixin, ObjectDeleteMixin, RESTObject): _id_attr = "id" class ProjectApprovalRuleManager( ListMixin, CreateMixin, UpdateMixin, DeleteMixin, RESTManager ): _path = "/projects/%(project_id)s/approval_rules" _obj_cls = ProjectApprovalRule _from_parent_attrs = {"project_id": "id"} _create_attrs = (("name", "approvals_required"), ("user_ids", "group_ids")) class ProjectDeployment(RESTObject, SaveMixin): pass class ProjectDeploymentManager(RetrieveMixin, CreateMixin, UpdateMixin, RESTManager): _path = "/projects/%(project_id)s/deployments" _obj_cls = ProjectDeployment _from_parent_attrs = {"project_id": "id"} _list_filters = ("order_by", "sort") _create_attrs = (("sha", "ref", "tag", "status", "environment"), tuple()) 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", "unprotect_access_level", "allowed_to_push", "allowed_to_merge", "allowed_to_unprotect", ), ) 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 ProjectExport(RefreshMixin, RESTObject): _id_attr = None @cli.register_custom_action("ProjectExport") @exc.on_http_error(exc.GitlabGetError) def download(self, streamed=False, action=None, chunk_size=1024, **kwargs): """Download the archive of a project export. Args: streamed (bool): If True the data will be processed by chunks of `chunk_size` and each chunk is passed to `action` for reatment 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/export/download" % self.project_id result = self.manager.gitlab.http_get( path, streamed=streamed, raw=True, **kwargs ) return utils.response_content(result, streamed, action, chunk_size) class ProjectExportManager(GetWithoutIdMixin, CreateMixin, RESTManager): _path = "/projects/%(project_id)s/export" _obj_cls = ProjectExport _from_parent_attrs = {"project_id": "id"} _create_attrs = (tuple(), ("description",)) class ProjectImport(RefreshMixin, RESTObject): _id_attr = None class ProjectImportManager(GetWithoutIdMixin, RESTManager): _path = "/projects/%(project_id)s/import" _obj_cls = ProjectImport _from_parent_attrs = {"project_id": "id"} class ProjectAdditionalStatistics(RefreshMixin, RESTObject): _id_attr = None class ProjectAdditionalStatisticsManager(GetWithoutIdMixin, RESTManager): _path = "/projects/%(project_id)s/statistics" _obj_cls = ProjectAdditionalStatistics _from_parent_attrs = {"project_id": "id"} class ProjectIssuesStatistics(RefreshMixin, RESTObject): _id_attr = None class ProjectIssuesStatisticsManager(GetWithoutIdMixin, RESTManager): _path = "/projects/%(project_id)s/issues_statistics" _obj_cls = ProjectIssuesStatistics _from_parent_attrs = {"project_id": "id"} class Project(SaveMixin, ObjectDeleteMixin, RESTObject): _short_print_attr = "path" _managers = ( ("accessrequests", "ProjectAccessRequestManager"), ("approvals", "ProjectApprovalManager"), ("approvalrules", "ProjectApprovalRuleManager"), ("badges", "ProjectBadgeManager"), ("boards", "ProjectBoardManager"), ("branches", "ProjectBranchManager"), ("jobs", "ProjectJobManager"), ("commits", "ProjectCommitManager"), ("customattributes", "ProjectCustomAttributeManager"), ("deployments", "ProjectDeploymentManager"), ("environments", "ProjectEnvironmentManager"), ("events", "ProjectEventManager"), ("exports", "ProjectExportManager"), ("files", "ProjectFileManager"), ("forks", "ProjectForkManager"), ("hooks", "ProjectHookManager"), ("keys", "ProjectKeyManager"), ("imports", "ProjectImportManager"), ("issues", "ProjectIssueManager"), ("labels", "ProjectLabelManager"), ("members", "ProjectMemberManager"), ("mergerequests", "ProjectMergeRequestManager"), ("milestones", "ProjectMilestoneManager"), ("notes", "ProjectNoteManager"), ("notificationsettings", "ProjectNotificationSettingsManager"), ("pagesdomains", "ProjectPagesDomainManager"), ("pipelines", "ProjectPipelineManager"), ("protectedbranches", "ProjectProtectedBranchManager"), ("protectedtags", "ProjectProtectedTagManager"), ("pipelineschedules", "ProjectPipelineScheduleManager"), ("pushrules", "ProjectPushRulesManager"), ("releases", "ProjectReleaseManager"), ("repositories", "ProjectRegistryRepositoryManager"), ("runners", "ProjectRunnerManager"), ("services", "ProjectServiceManager"), ("snippets", "ProjectSnippetManager"), ("tags", "ProjectTagManager"), ("users", "ProjectUserManager"), ("triggers", "ProjectTriggerManager"), ("variables", "ProjectVariableManager"), ("wikis", "ProjectWikiManager"), ("clusters", "ProjectClusterManager"), ("additionalstatistics", "ProjectAdditionalStatisticsManager"), ("issuesstatistics", "ProjectIssuesStatisticsManager"), ) @cli.register_custom_action("Project", ("submodule", "branch", "commit_sha")) @exc.on_http_error(exc.GitlabUpdateError) def update_submodule(self, submodule, branch, commit_sha, **kwargs): """Update a project submodule Args: submodule (str): Full path to the submodule branch (str): Name of the branch to commit into commit_sha (str): Full commit SHA to update the submodule to commit_message (str): Commit message. If no message is provided, a default one will be set (optional) Raises: GitlabAuthenticationError: If authentication is not correct GitlabPutError: If the submodule could not be updated """ submodule = submodule.replace("/", "%2F") # .replace('.', '%2E') path = "/projects/%s/repository/submodules/%s" % (self.get_id(), submodule) data = {"branch": branch, "commit_sha": commit_sha} if "commit_message" in kwargs: data["commit_message"] = kwargs["commit_message"] return self.manager.gitlab.http_put(path, post_data=data) @cli.register_custom_action("Project", tuple(), ("path", "ref", "recursive")) @exc.on_http_error(exc.GitlabGetError) def repository_tree(self, path="", ref="", recursive=False, **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 recursive (bool): Whether to get the tree recursively 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 = {"recursive": recursive} 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, raw=True, **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, raw=True, 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.GitlabDeleteError) def delete_merged_branches(self, **kwargs): """Delete merged branches. 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/repository/merged_branches" % self.get_id() self.manager.gitlab.http_delete(path, **kwargs) @cli.register_custom_action("Project") @exc.on_http_error(exc.GitlabGetError) def languages(self, **kwargs): """Get languages used in the project with percentage value. Args: **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 """ path = "/projects/%s/languages" % self.get_id() return self.manager.gitlab.http_get(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) @cli.register_custom_action("Project", ("group_id",)) @exc.on_http_error(exc.GitlabDeleteError) def unshare(self, group_id, **kwargs): """Delete a shared project link within a group. Args: group_id (int): ID of the group. **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/share/%s" % (self.get_id(), group_id) self.manager.gitlab.http_delete(path, **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=None, **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 """ variables = variables or {} 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"]} @cli.register_custom_action("Project", optional=("wiki",)) @exc.on_http_error(exc.GitlabGetError) def snapshot( self, wiki=False, streamed=False, action=None, chunk_size=1024, **kwargs ): """Return a snapshot of the repository. Args: wiki (bool): If True return the wiki repository 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 uncompressed tar archive of the repository """ path = "/projects/%s/snapshot" % self.get_id() result = self.manager.gitlab.http_get( path, streamed=streamed, raw=True, **kwargs ) return utils.response_content(result, streamed, action, chunk_size) @cli.register_custom_action("Project", ("scope", "search")) @exc.on_http_error(exc.GitlabSearchError) def search(self, scope, search, **kwargs): """Search the project resources matching the provided string.' Args: scope (str): Scope of the search search (str): Search string **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabSearchError: If the server failed to perform the request Returns: GitlabList: A list of dicts describing the resources found. """ data = {"scope": scope, "search": search} path = "/projects/%s/search" % self.get_id() return self.manager.gitlab.http_list(path, query_data=data, **kwargs) @cli.register_custom_action("Project") @exc.on_http_error(exc.GitlabCreateError) def mirror_pull(self, **kwargs): """Start the pull mirroring process for the 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/mirror/pull" % self.get_id() self.manager.gitlab.http_post(path, **kwargs) @cli.register_custom_action("Project", ("to_namespace",)) @exc.on_http_error(exc.GitlabTransferProjectError) def transfer_project(self, to_namespace, **kwargs): """Transfer a project to the given namespace ID Args: to_namespace (str): ID or path of the namespace to transfer the project to **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 = "/projects/%s/transfer" % (self.id,) self.manager.gitlab.http_put( path, post_data={"namespace": to_namespace}, **kwargs ) @cli.register_custom_action("Project", ("ref_name", "artifact_path", "job")) @exc.on_http_error(exc.GitlabGetError) def artifact( self, ref_name, artifact_path, job, streamed=False, action=None, chunk_size=1024, **kwargs ): """Download a single artifact file from a specific tag or branch from within the job’s artifacts archive. Args: ref_name (str): Branch or tag name in repository. HEAD or SHA references are not supported. artifact_path (str): Path to a file inside the artifacts archive. job (str): The name of the job. 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 = "/projects/%s/jobs/artifacts/%s/raw/%s?job=%s" % ( self.get_id(), ref_name, artifact_path, job, ) result = self.manager.gitlab.http_get( path, streamed=streamed, raw=True, **kwargs ) return utils.response_content(result, streamed, action, chunk_size) class ProjectManager(CRUDMixin, RESTManager): _path = "/projects" _obj_cls = Project _create_attrs = ( tuple(), ( "name", "path", "namespace_id", "description", "issues_enabled", "merge_requests_enabled", "jobs_enabled", "wiki_enabled", "snippets_enabled", "resolve_outdated_diff_discussions", "container_registry_enabled", "shared_runners_enabled", "visibility", "import_url", "public_jobs", "only_allow_merge_if_pipeline_succeeds", "only_allow_merge_if_all_discussions_are_resolved", "merge_method", "lfs_enabled", "request_access_enabled", "tag_list", "avatar", "printing_merge_request_link_enabled", "ci_config_path", ), ) _update_attrs = ( tuple(), ( "name", "path", "default_branch", "description", "issues_enabled", "merge_requests_enabled", "jobs_enabled", "wiki_enabled", "snippets_enabled", "resolve_outdated_diff_discussions", "container_registry_enabled", "shared_runners_enabled", "visibility", "import_url", "public_jobs", "only_allow_merge_if_pipeline_succeeds", "only_allow_merge_if_all_discussions_are_resolved", "merge_method", "lfs_enabled", "request_access_enabled", "tag_list", "avatar", "ci_config_path", ), ) _types = {"avatar": types.ImageAttribute} _list_filters = ( "search", "owned", "starred", "archived", "visibility", "order_by", "sort", "simple", "membership", "statistics", "with_issues_enabled", "with_merge_requests_enabled", "with_custom_attributes", ) def import_project( self, file, path, namespace=None, overwrite=False, override_params=None, **kwargs ): """Import a project from an archive file. Args: file: Data or file object containing the project path (str): Name and path for the new project namespace (str): The ID or path of the namespace that the project will be imported to overwrite (bool): If True overwrite an existing project with the same path override_params (dict): Set the specific settings for the project **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: dict: A representation of the import status. """ files = {"file": ("file.tar.gz", file)} data = {"path": path, "overwrite": overwrite} if override_params: for k, v in override_params.items(): data["override_params[%s]" % k] = v if namespace: data["namespace"] = namespace return self.gitlab.http_post( "/projects/import", post_data=data, files=files, **kwargs ) def import_github( self, personal_access_token, repo_id, target_namespace, new_name=None, **kwargs ): """Import a project from Github to Gitlab (schedule the import) This method will return when an import operation has been safely queued, or an error has occurred. After triggering an import, check the `import_status` of the newly created project to detect when the import operation has completed. NOTE: this request may take longer than most other API requests. So this method will specify a 60 second default timeout if none is specified. A timeout can be specified via kwargs to override this functionality. Args: personal_access_token (str): GitHub personal access token repo_id (int): Github repository ID target_namespace (str): Namespace to import repo into new_name (str): New repo name (Optional) **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: dict: A representation of the import status. Example: ``` gl = gitlab.Gitlab_from_config() print "Triggering import" result = gl.projects.import_github(ACCESS_TOKEN, 123456, "my-group/my-subgroup") project = gl.projects.get(ret['id']) print "Waiting for import to complete" while project.import_status == u'started': time.sleep(1.0) project = gl.projects.get(project.id) print "Github import complete" ``` """ data = { "personal_access_token": personal_access_token, "repo_id": repo_id, "target_namespace": target_namespace, } if new_name: data["new_name"] = new_name if ( "timeout" not in kwargs or self.gitlab.timeout is None or self.gitlab.timeout < 60.0 ): # Ensure that this HTTP request has a longer-than-usual default timeout # The base gitlab object tends to have a default that is <10 seconds, # and this is too short for this API command, typically. # On the order of 24 seconds has been measured on a typical gitlab instance. kwargs["timeout"] = 60.0 result = self.gitlab.http_post("/import/github", post_data=data, **kwargs) return result class RunnerJob(RESTObject): pass class RunnerJobManager(ListMixin, RESTManager): _path = "/runners/%(runner_id)s/jobs" _obj_cls = RunnerJob _from_parent_attrs = {"runner_id": "id"} _list_filters = ("status",) class Runner(SaveMixin, ObjectDeleteMixin, RESTObject): _managers = (("jobs", "RunnerJobManager"),) class RunnerManager(CRUDMixin, RESTManager): _path = "/runners" _obj_cls = Runner _list_filters = ("scope",) _create_attrs = ( ("token",), ( "description", "info", "active", "locked", "run_untagged", "tag_list", "maximum_timeout", ), ) _update_attrs = ( tuple(), ( "description", "active", "tag_list", "run_untagged", "locked", "access_level", "maximum_timeout", ), ) @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) @cli.register_custom_action("RunnerManager", ("token",)) @exc.on_http_error(exc.GitlabVerifyError) def verify(self, token, **kwargs): """Validates authentication credentials for a registered Runner. Args: token (str): The runner's authentication token **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabVerifyError: If the server failed to verify the token """ path = "/runners/verify" post_data = {"token": token} self.gitlab.http_post(path, post_data=post_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(ListMixin, 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) class GeoNode(SaveMixin, ObjectDeleteMixin, RESTObject): @cli.register_custom_action("GeoNode") @exc.on_http_error(exc.GitlabRepairError) def repair(self, **kwargs): """Repair the OAuth authentication of the geo node. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabRepairError: If the server failed to perform the request """ path = "/geo_nodes/%s/repair" % self.get_id() server_data = self.manager.gitlab.http_post(path, **kwargs) self._update_attrs(server_data) @cli.register_custom_action("GeoNode") @exc.on_http_error(exc.GitlabGetError) def status(self, **kwargs): """Get the status of the geo node. Args: **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 status of the geo node """ path = "/geo_nodes/%s/status" % self.get_id() return self.manager.gitlab.http_get(path, **kwargs) class GeoNodeManager(RetrieveMixin, UpdateMixin, DeleteMixin, RESTManager): _path = "/geo_nodes" _obj_cls = GeoNode _update_attrs = ( tuple(), ("enabled", "url", "files_max_capacity", "repos_max_capacity"), ) @cli.register_custom_action("GeoNodeManager") @exc.on_http_error(exc.GitlabGetError) def status(self, **kwargs): """Get the status of all the geo nodes. Args: **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 status of all the geo nodes """ return self.gitlab.http_list("/geo_nodes/status", **kwargs) @cli.register_custom_action("GeoNodeManager") @exc.on_http_error(exc.GitlabGetError) def current_failures(self, **kwargs): """Get the list of failures on the current geo node. Args: **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 list of failures """ return self.gitlab.http_list("/geo_nodes/current/failures", **kwargs) python-gitlab-2.0.1/requirements.txt000066400000000000000000000000211361651701000175500ustar00rootroot00000000000000requests>=2.22.0 python-gitlab-2.0.1/rtd-requirements.txt000066400000000000000000000000511361651701000203420ustar00rootroot00000000000000-r requirements.txt jinja2 sphinx>=1.7.6 python-gitlab-2.0.1/setup.py000066400000000000000000000030311361651701000160020ustar00rootroot00000000000000#!/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]) with open("README.rst", "r") as readme_file: readme = readme_file.read() setup( name="python-gitlab", version=get_version(), description="Interact with GitLab API", long_description=readme, author="Gauvain Pocentek", author_email="gauvain@pocentek.net", license="LGPLv3", url="https://github.com/python-gitlab/python-gitlab", packages=find_packages(), install_requires=["requests>=2.22.0"], python_requires=">=3.6.0", 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 :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", ], extras_require={ "autocompletion": ["argcomplete>=1.10.0,<2"], "yaml": ["PyYaml>=5.2"], }, ) python-gitlab-2.0.1/test-requirements.txt000066400000000000000000000001471361651701000205360ustar00rootroot00000000000000coverage discover testrepository hacking>=0.9.2,<0.10 httmock jinja2 mock sphinx>=1.3 sphinx_rtd_theme python-gitlab-2.0.1/tools/000077500000000000000000000000001361651701000154335ustar00rootroot00000000000000python-gitlab-2.0.1/tools/avatar.png000066400000000000000000000011201361651701000174110ustar00rootroot00000000000000‰PNG  IHDRóÿaIDAT8Ëm’Ûn£0Eø‚CRQ)ÿÿ]U¥ö¡D¤ %„› óÀÀ´3ãGËgËg¯åÍó<LÓÄ8ŽŒãÈ0 8瘦 !RJ´Ö(¥PJ!„@®ÃÃ0ÐumÛÒ4 mÛþØívDQÄn·cžg´ÖKð:Ü4 ×땪ªøúú¢®kº®ÀCÇ$I‚µv ÖZ#st]Çívãr¹P§Ó‰²,iš€(Šx||$ÏsæyÆ÷}„ø¾Ç‘®ë¨ªŠ¢(x}}ååå…Óéô# Ïsú¾ßVZ»Î¹mïËåÂÛÛÏÏϼ¿¿S×5qs½^‘RÇ1ÖÚ­éœc꺦,KÎç3EÁçç'Y–¡µæ|>S–%ûýž4MqÎ-æyæ~¿³öÑ4 u]SUÕ‚JJš¦¡ë:œsÜïw~Ó_¾Ÿ•·ÖcÌÖ¶Ö)ÿyŽ\% Ã,ËH’„$I8(¥¶»$IȲŒ0 ‘R®‚ÉM’4M9´m‹Öú…§§'ŽÇ#išþ)PJ¤R c ÖZ†a`š&¤”¤iú߀<ϱÖbŒY0J)1ÆüpßÃápàv»†!ÖZ²,c¿ßEƘåB‚ Ø SJEY–1Ž#Àv÷ðð°}?„xórX}èûž¾ï·uVó´ÖA@Ï󖀿]˜¦ çÜÆÚó¼ï­ãû>žçð Tö.ä]ÿäíIEND®B`‚python-gitlab-2.0.1/tools/build_test_env.sh000077500000000000000000000116311361651701000210020ustar00rootroot00000000000000#!/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"; } REUSE_CONTAINER= NOVENV= PY_VER=3 API_VER=4 while getopts :knp:a: opt "$@"; do case $opt in k) REUSE_CONTAINER=1;; 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="python3 -m venv";; *) fatal "Wrong python version (2 or 3)";; esac case $API_VER in 4) ;; *) fatal "Wrong API version (4 only)";; esac for req in \ curl \ docker \ ; 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" if [ -z "$REUSE_CONTAINER" ]; then log "Stopping gitlab-test docker container..." docker rm -f gitlab-test >/dev/null fi log "Done." } [ -z "${BUILD_TEST_ENV_AUTO_CLEANUP+set}" ] || { trap cleanup EXIT trap 'exit 1' HUP INT TERM } if [ -z "$REUSE_CONTAINER" ] || ! docker top gitlab-test >/dev/null 2>&1; then GITLAB_OMNIBUS_CONFIG="external_url 'http://gitlab.test' gitlab_rails['initial_root_password'] = '5iveL!fe' gitlab_rails['initial_shared_runners_registration_token'] = 'sTPNtWLEuSrHzoHP8oCU' registry['enable'] = false nginx['redirect_http_to_https'] = false nginx['listen_port'] = 80 nginx['listen_https'] = false pages_external_url 'http://pages.gitlab.lxd' gitlab_pages['enable'] = true gitlab_pages['inplace_chroot'] = true prometheus['enable'] = false alertmanager['enable'] = false node_exporter['enable'] = false redis_exporter['enable'] = false postgres_exporter['enable'] = false pgbouncer_exporter['enable'] = false gitlab_exporter['enable'] = false grafana['enable'] = false letsencrypt['enable'] = false " try docker run --name gitlab-test --detach --publish 8080:80 \ --publish 2222:22 --env "GITLAB_OMNIBUS_CONFIG=$GITLAB_OMNIBUS_CONFIG" \ gitlab/gitlab-ce:latest >/dev/null fi 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 -r requirements.txt log "Installing into virtualenv..." try pip install -e . # to run generate_token.py pip install requests-html 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 # last command started by the container is "gitlab-ctl tail" docker exec gitlab-test pgrep -f 'gitlab-ctl tail' &>/dev/null \ && docker exec gitlab-test curl http://localhost/-/health 2>/dev/null \ | grep -q 'GitLab OK' \ && curl -s http://localhost:8080/users/sign_in 2>/dev/null \ | grep -q "GitLab Community Edition" \ && break I=$((I+5)) [ "$I" -lt 180 ] || fatal "timed out" done log "Pausing to give GitLab some time to finish starting up..." sleep 200 # Get the token TOKEN=$($(dirname $0)/generate_token.py) cat > $CONFIG << EOF [global] default = local timeout = 30 [local] url = http://localhost:8080 private_token = $TOKEN api_version = $API_VER EOF log "Config file content ($CONFIG):" log <$CONFIG if [ ! -z "$REUSE_CONTAINER" ]; then echo reset gitlab $(dirname $0)/reset_gitlab.py fi log "Test environment initialized." python-gitlab-2.0.1/tools/cli_test_v4.sh000077500000000000000000000144201361651701000202120ustar00rootroot00000000000000#!/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 "group creation" ' OUTPUT=$(try GITLAB group create --name test-group1 --path group1) || exit 1 GROUP_ID=$(pecho "${OUTPUT}" | grep ^id: | cut -d" " -f2) OUTPUT=$(try GITLAB group list) || exit 1 pecho "${OUTPUT}" | grep -q test-group1 ' testcase "group update" ' GITLAB group update --id "$GROUP_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 ' # Test project labels testcase "create project label" ' OUTPUT=$(GITLAB -v project-label create --project-id $PROJECT_ID \ --name prjlabel1 --description "prjlabel1 description" --color "#112233") ' testcase "list project label" ' OUTPUT=$(GITLAB -v project-label list --project-id $PROJECT_ID) ' testcase "update project label" ' OUTPUT=$(GITLAB -v project-label update --project-id $PROJECT_ID \ --name prjlabel1 --new-name prjlabel2 --description "prjlabel2 description" --color "#332211") ' testcase "delete project label" ' OUTPUT=$(GITLAB -v project-label delete --project-id $PROJECT_ID \ --name prjlabel2) ' # Test group labels testcase "create group label" ' OUTPUT=$(GITLAB -v group-label create --group-id $GROUP_ID \ --name grplabel1 --description "grplabel1 description" --color "#112233") ' testcase "list group label" ' OUTPUT=$(GITLAB -v group-label list --group-id $GROUP_ID) ' testcase "update group label" ' OUTPUT=$(GITLAB -v group-label update --group-id $GROUP_ID \ --name grplabel1 --new-name grplabel2 --description "grplabel2 description" --color "#332211") ' testcase "delete group label" ' OUTPUT=$(GITLAB -v group-label delete --group-id $GROUP_ID \ --name grplabel2) ' # Test project variables testcase "create project variable" ' OUTPUT=$(GITLAB -v project-variable create --project-id $PROJECT_ID \ --key junk --value car) ' testcase "get project variable" ' OUTPUT=$(GITLAB -v project-variable get --project-id $PROJECT_ID \ --key junk) ' testcase "update project variable" ' OUTPUT=$(GITLAB -v project-variable update --project-id $PROJECT_ID \ --key junk --value bus) ' testcase "list project variable" ' OUTPUT=$(GITLAB -v project-variable list --project-id $PROJECT_ID) ' testcase "delete project variable" ' OUTPUT=$(GITLAB -v project-variable delete --project-id $PROJECT_ID \ --key junk) ' 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 "group deletion" ' OUTPUT=$(try GITLAB group delete --id $GROUP_ID) ' testcase "application settings get" ' GITLAB application-settings get >/dev/null 2>&1 ' testcase "application settings update" ' GITLAB application-settings update --signup-enabled false >/dev/null 2>&1 ' cat > /tmp/gitlab-project-description << EOF Multi line Data EOF testcase "values from files" ' OUTPUT=$(GITLAB -v project create --name fromfile \ --description @/tmp/gitlab-project-description) echo $OUTPUT | grep -q "Multi line" ' python-gitlab-2.0.1/tools/ee-test.py000077500000000000000000000076671361651701000173760ustar00rootroot00000000000000#!/usr/bin/env python import gitlab P1 = "root/project1" P2 = "root/project2" MR_P1 = 1 I_P1 = 1 I_P2 = 1 EPIC_ISSUES = [4, 5] G1 = "group1" LDAP_CN = "app1" LDAP_PROVIDER = "ldapmain" def start_log(message): print("Testing %s... " % message, end="") def end_log(): print("OK") gl = gitlab.Gitlab.from_config("ee") project1 = gl.projects.get(P1) project2 = gl.projects.get(P2) issue_p1 = project1.issues.get(I_P1) issue_p2 = project2.issues.get(I_P2) group1 = gl.groups.get(G1) mr = project1.mergerequests.get(1) start_log("MR approvals") approval = project1.approvals.get() v = approval.reset_approvals_on_push approval.reset_approvals_on_push = not v approval.save() approval = project1.approvals.get() assert v != approval.reset_approvals_on_push project1.approvals.set_approvers([1], []) approval = project1.approvals.get() assert approval.approvers[0]["user"]["id"] == 1 approval = mr.approvals.get() approval.approvals_required = 2 approval.save() approval = mr.approvals.get() assert approval.approvals_required == 2 approval.approvals_required = 3 approval.save() approval = mr.approvals.get() assert approval.approvals_required == 3 mr.approvals.set_approvers([1], []) approval = mr.approvals.get() assert approval.approvers[0]["user"]["id"] == 1 ars = project1.approvalrules.list(all=True) assert len(ars) == 0 project.approvalrules.create( {"name": "approval-rule", "approvals_required": 1, "group_ids": [group1.id]} ) ars = project1.approvalrules.list(all=True) assert len(ars) == 1 ars[0].approvals_required == 2 ars[0].save() ars = project1.approvalrules.list(all=True) assert len(ars) == 1 assert ars[0].approvals_required == 2 ars[0].delete() ars = project1.approvalrules.list(all=True) assert len(ars) == 0 end_log() start_log("geo nodes") # very basic tests because we only have 1 node... nodes = gl.geonodes.list() status = gl.geonodes.status() end_log() start_log("issue links") # bit of cleanup just in case for link in issue_p1.links.list(): issue_p1.links.delete(link.issue_link_id) src, dst = issue_p1.links.create({"target_project_id": P2, "target_issue_iid": I_P2}) links = issue_p1.links.list() link_id = links[0].issue_link_id issue_p1.links.delete(link_id) end_log() start_log("LDAP links") # bit of cleanup just in case if hasattr(group1, "ldap_group_links"): for link in group1.ldap_group_links: group1.delete_ldap_group_link(link["cn"], link["provider"]) assert gl.ldapgroups.list() group1.add_ldap_group_link(LDAP_CN, 30, LDAP_PROVIDER) group1.ldap_sync() group1.delete_ldap_group_link(LDAP_CN) end_log() start_log("boards") # bit of cleanup just in case for board in project1.boards.list(): if board.name == "testboard": board.delete() board = project1.boards.create({"name": "testboard"}) board = project1.boards.get(board.id) project1.boards.delete(board.id) for board in group1.boards.list(): if board.name == "testboard": board.delete() board = group1.boards.create({"name": "testboard"}) board = group1.boards.get(board.id) group1.boards.delete(board.id) end_log() start_log("push rules") pr = project1.pushrules.get() if pr: pr.delete() pr = project1.pushrules.create({"deny_delete_tag": True}) pr.deny_delete_tag = False pr.save() pr = project1.pushrules.get() assert pr is not None assert pr.deny_delete_tag == False pr.delete() end_log() start_log("license") l = gl.get_license() assert "user_limit" in l try: gl.set_license("dummykey") except Exception as e: assert "The license key is invalid." in e.error_message end_log() start_log("epics") epic = group1.epics.create({"title": "Test epic"}) epic.title = "Fixed title" epic.labels = ["label1", "label2"] epic.save() epic = group1.epics.get(epic.iid) assert epic.title == "Fixed title" assert len(group1.epics.list()) # issues assert not epic.issues.list() for i in EPIC_ISSUES: epic.issues.create({"issue_id": i}) assert len(EPIC_ISSUES) == len(epic.issues.list()) for ei in epic.issues.list(): ei.delete() epic.delete() end_log() python-gitlab-2.0.1/tools/functional_tests.sh000077500000000000000000000016011361651701000213540ustar00rootroot00000000000000#!/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-2.0.1/tools/generate_token.py000077500000000000000000000030121361651701000207760ustar00rootroot00000000000000#!/usr/bin/env python from urllib.parse import urljoin from requests_html import HTMLSession ENDPOINT = "http://localhost:8080" LOGIN = "root" PASSWORD = "5iveL!fe" class GitlabSession(HTMLSession): def __init__(self, endpoint, *args, **kwargs): super().__init__(*args, **kwargs) self.endpoint = endpoint self.csrf = None def find_csrf_token(self, html): param = html.find("meta[name=csrf-param]")[0].attrs["content"] token = html.find("meta[name=csrf-token]")[0].attrs["content"] self.csrf = {param: token} def obtain_csrf_token(self): r = self.get(urljoin(self.endpoint, "/")) self.find_csrf_token(r.html) def sign_in(self, login, password): data = {"user[login]": login, "user[password]": password, **self.csrf} r = self.post(urljoin(self.endpoint, "/users/sign_in"), data=data) self.find_csrf_token(r.html) def obtain_personal_access_token(self, name): data = { "personal_access_token[name]": name, "personal_access_token[scopes][]": ["api", "sudo"], **self.csrf, } r = self.post( urljoin(self.endpoint, "/profile/personal_access_tokens"), data=data ) return r.html.find("#created-personal-access-token")[0].attrs["value"] def main(): with GitlabSession(ENDPOINT) as s: s.obtain_csrf_token() s.sign_in(LOGIN, PASSWORD) print(s.obtain_personal_access_token("default")) if __name__ == "__main__": main() python-gitlab-2.0.1/tools/py_functional_tests.sh000077500000000000000000000016171361651701000220730ustar00rootroot00000000000000#!/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-2.0.1/tools/python_test_v4.py000066400000000000000000000734441361651701000210120ustar00rootroot00000000000000import base64 import os import time import requests 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-----""" AVATAR_PATH = os.path.join(os.path.dirname(__file__), "avatar.png") # 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) # markdown html = gl.markdown("foo") assert "foo" in html success, errors = gl.lint("Invalid") assert success is False assert errors # 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", "avatar": open(AVATAR_PATH, "rb"), } ) avatar_url = new_user.avatar_url.replace("gitlab.test", "localhost:8080") uploaded_avatar = requests.get(avatar_url).content assert uploaded_avatar == open(AVATAR_PATH, "rb").read() 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 expected = [new_user, foobar_user] actual = list(gl.users.list(search="foo")) 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 + len( [u for u in gl.users.list() if u.username == "ghost"] ) # 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.const.OWNER_ACCESS, "user_id": user1.id}) group1.members.create({"access_level": gitlab.const.GUEST_ACCESS, "user_id": user2.id}) group2.members.create({"access_level": gitlab.const.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 assert len(group1.members.all()) member = group1.members.get(user2.id) member.access_level = gitlab.const.OWNER_ACCESS member.save() member = group1.members.get(user2.id) assert member.access_level == gitlab.const.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 badges badge_image = "http://example.com" badge_link = "http://example/img.svg" badge = group2.badges.create({"link_url": badge_link, "image_url": badge_image}) assert len(group2.badges.list()) == 1 badge.image_url = "http://another.example.com" badge.save() badge = group2.badges.get(badge.id) assert badge.image_url == "http://another.example.com" badge.delete() assert len(group2.badges.list()) == 0 # 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 # group labels # group1.labels.create({"name": "foo", "description": "bar", "color": "#112233"}) # g_l = group1.labels.get("foo") # assert g_l.description == "bar" # g_l.description = "baz" # g_l.save() # g_l = group1.labels.get("foo") # assert g_l.description == "baz" # assert len(group1.labels.list()) == 1 # g_l.delete() # assert len(group1.labels.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(b"Improved README").decode() 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") # The first decode() is the ProjectFile method, the second one is the bytes # object method assert readme.decode().decode() == "Initial content" blame = admin_project.files.blame(file_path="README.rst", ref="master") 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] # size = len(commit.statuses.list()) # status = commit.statuses.create({"state": "success", "sha": commit.id}) # assert len(commit.statuses.list()) == size + 1 # assert commit.refs() # assert commit.merge_requests() # commit comment commit.comments.create({"note": "This is a commit comment"}) # assert len(commit.comments.list()) == 1 # commit discussion count = len(commit.discussions.list()) discussion = commit.discussions.create({"body": "Discussion body"}) # assert len(commit.discussions.list()) == (count + 1) d_note = discussion.notes.create({"body": "first note"}) d_note_from_get = discussion.notes.get(d_note.id) d_note_from_get.body = "updated body" d_note_from_get.save() discussion = commit.discussions.get(discussion.id) # assert discussion.attributes["notes"][-1]["body"] == "updated body" d_note_from_get.delete() discussion = commit.discussions.get(discussion.id) # assert len(discussion.attributes["notes"]) == 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.decode() == "Initial content" archive1 = admin_project.repository_archive() archive2 = admin_project.repository_archive("master") assert archive1 == archive2 snapshot = admin_project.snapshot() # 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 = envs[0] env.external_url = "http://new.env/whatever" env.save() env = admin_project.environments.list()[0] assert env.external_url == "http://new.env/whatever" env.stop() env.delete() assert len(admin_project.environments.list()) == 0 # Project clusters admin_project.clusters.create( { "name": "cluster1", "platform_kubernetes_attributes": { "api_url": "http://url", "token": "tokenval", }, } ) clusters = admin_project.clusters.list() assert len(clusters) == 1 cluster = clusters[0] cluster.platform_kubernetes_attributes = {"api_url": "http://newurl"} cluster.save() cluster = admin_project.clusters.list()[0] assert cluster.platform_kubernetes["api_url"] == "http://newurl" cluster.delete() assert len(admin_project.clusters.list()) == 0 # Group clusters group1.clusters.create( { "name": "cluster1", "platform_kubernetes_attributes": { "api_url": "http://url", "token": "tokenval", }, } ) clusters = group1.clusters.list() assert len(clusters) == 1 cluster = clusters[0] cluster.platform_kubernetes_attributes = {"api_url": "http://newurl"} cluster.save() cluster = group1.clusters.list()[0] assert cluster.platform_kubernetes["api_url"] == "http://newurl" cluster.delete() assert len(group1.clusters.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 forks = admin_project.forks.list() assert fork.id in map(lambda p: p.id, forks) # 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.list()[0] # 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" size = len(issue1.notes.list()) note = issue1.notes.create({"body": "This is an issue note"}) assert len(issue1.notes.list()) == size + 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()) == size assert isinstance(issue1.user_agent_detail(), dict) assert issue1.user_agent_detail()["user_agent"] assert issue1.participants() assert type(issue1.closed_by()) == list assert type(issue1.related_merge_requests()) == list # issues labels and events label2 = admin_project.labels.create({"name": "label2", "color": "#aabbcc"}) issue1.labels = ["label2"] issue1.save() events = issue1.resourcelabelevents.list() assert events event = issue1.resourcelabelevents.get(events[0].id) assert event size = len(issue1.discussions.list()) discussion = issue1.discussions.create({"body": "Discussion body"}) assert len(issue1.discussions.list()) == size + 1 d_note = discussion.notes.create({"body": "first note"}) d_note_from_get = discussion.notes.get(d_note.id) d_note_from_get.body = "updated body" d_note_from_get.save() discussion = issue1.discussions.get(discussion.id) assert discussion.attributes["notes"][-1]["body"] == "updated body" d_note_from_get.delete() discussion = issue1.discussions.get(discussion.id) assert len(discussion.attributes["notes"]) == 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() # project snippet admin_project.snippets_enabled = True admin_project.save() snippet = admin_project.snippets.create( { "title": "snip1", "file_name": "foo.py", "content": "initial content", "visibility": gitlab.v4.objects.VISIBILITY_PRIVATE, } ) assert snippet.user_agent_detail()["user_agent"] size = len(snippet.discussions.list()) discussion = snippet.discussions.create({"body": "Discussion body"}) assert len(snippet.discussions.list()) == size + 1 d_note = discussion.notes.create({"body": "first note"}) d_note_from_get = discussion.notes.get(d_note.id) d_note_from_get.body = "updated body" d_note_from_get.save() discussion = snippet.discussions.get(discussion.id) assert discussion.attributes["notes"][-1]["body"] == "updated body" d_note_from_get.delete() discussion = snippet.discussions.get(discussion.id) assert len(discussion.attributes["notes"]) == 1 snippet.file_name = "bar.py" snippet.save() snippet = admin_project.snippets.get(snippet.id) assert snippet.content().decode() == "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"} ) # discussion size = len(mr.discussions.list()) discussion = mr.discussions.create({"body": "Discussion body"}) assert len(mr.discussions.list()) == size + 1 d_note = discussion.notes.create({"body": "first note"}) d_note_from_get = discussion.notes.get(d_note.id) d_note_from_get.body = "updated body" d_note_from_get.save() discussion = mr.discussions.get(discussion.id) assert discussion.attributes["notes"][-1]["body"] == "updated body" d_note_from_get.delete() discussion = mr.discussions.get(discussion.id) assert len(discussion.attributes["notes"]) == 1 # mr labels and events mr.labels = ["label2"] mr.save() events = mr.resourcelabelevents.list() assert events event = mr.resourcelabelevents.get(events[0].id) assert event # rebasing assert mr.rebase() # basic testing: only make sure that the methods exist mr.commits() mr.changes() assert mr.participants() 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 badges badge_image = "http://example.com" badge_link = "http://example/img.svg" badge = admin_project.badges.create({"link_url": badge_link, "image_url": badge_image}) assert len(admin_project.badges.list()) == 1 badge.image_url = "http://another.example.com" badge.save() badge = admin_project.badges.get(badge.id) assert badge.image_url == "http://another.example.com" badge.delete() assert len(admin_project.badges.list()) == 0 # 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 # Disabled as this fails with GitLab 11.11 # feat = gl.features.set("foo", 30) # assert feat.name == "foo" # assert len(gl.features.list()) == 1 # feat.delete() # assert len(gl.features.list()) == 0 # broadcast messages msg = gl.broadcastmessages.create({"message": "this is the message"}) msg.color = "#444444" msg.save() msg_id = msg.id msg = gl.broadcastmessages.list(all=True)[0] assert msg.color == "#444444" msg = gl.broadcastmessages.get(msg_id) 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.decode() == "import gitlab" assert snippet.user_agent_detail()["user_agent"] snippet.delete() snippets = gl.snippets.list(all=True) assert len(snippets) == 0 # user activities gl.user_activities.list(query_parameters={"from": "2019-01-01"}) # events gl.events.list() # rate limit settings = gl.settings.get() settings.throttle_authenticated_api_enabled = True settings.throttle_authenticated_api_requests_per_period = 1 settings.throttle_authenticated_api_period_in_seconds = 3 settings.save() projects = list() for i in range(0, 20): projects.append(gl.projects.create({"name": str(i) + "ok"})) error_message = None for i in range(20, 40): try: projects.append( gl.projects.create({"name": str(i) + "shouldfail"}, obey_rate_limit=False) ) except gitlab.GitlabCreateError as e: error_message = e.error_message break assert "Retry later" in error_message settings.throttle_authenticated_api_enabled = False settings.save() [current_project.delete() for current_project in projects] # project import/export ex = admin_project.exports.create({}) ex.refresh() count = 0 while ex.export_status != "finished": time.sleep(1) ex.refresh() count += 1 if count == 10: raise Exception("Project export taking too much time") with open("/tmp/gitlab-export.tgz", "wb") as f: ex.download(streamed=True, action=f.write) output = gl.projects.import_project( open("/tmp/gitlab-export.tgz", "rb"), "imported_project" ) project_import = gl.projects.get(output["id"], lazy=True).imports.get() count = 0 while project_import.import_status != "finished": time.sleep(1) project_import.refresh() count += 1 if count == 10: raise Exception("Project import taking too much time") # project releases release_test_project = gl.projects.create( {"name": "release-test-project", "initialize_with_readme": True} ) release_name = "Demo Release" release_tag_name = "v1.2.3" release_description = "release notes go here" release_test_project.releases.create( { "name": release_name, "tag_name": release_tag_name, "description": release_description, "ref": "master", } ) assert len(release_test_project.releases.list()) == 1 # get single release retrieved_project = release_test_project.releases.get(release_tag_name) assert retrieved_project.name == release_name assert retrieved_project.tag_name == release_tag_name assert retrieved_project.description == release_description # delete release release_test_project.releases.delete(release_tag_name) assert len(release_test_project.releases.list()) == 0 release_test_project.delete() # status message = "Test" emoji = "thumbsup" status = gl.user.status.get() status.message = message status.emoji = emoji status.save() new_status = gl.user.status.get() assert new_status.message == message assert new_status.emoji == emoji python-gitlab-2.0.1/tools/reset_gitlab.py000077500000000000000000000006761361651701000204650ustar00rootroot00000000000000#!/usr/bin/env python import sys from gitlab import Gitlab def main(): with Gitlab.from_config(config_files=["/tmp/python-gitlab.cfg"]) as gl: for project in gl.projects.list(): project.delete() for group in gl.groups.list(): group.delete() for user in gl.users.list(): if user.username != "root": user.delete() if __name__ == "__main__": sys.exit(main()) python-gitlab-2.0.1/tox.ini000066400000000000000000000021071361651701000156060ustar00rootroot00000000000000[tox] minversion = 1.6 skipsdist = True envlist = py38,py37,py36,pep8,black [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:black] basepython = python3 deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt black commands = black {posargs} gitlab [testenv:venv] commands = {posargs} [flake8] exclude = .git,.venv,.tox,dist,doc,*egg,build, max-line-length = 88 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_v4] commands = {toxinidir}/tools/functional_tests.sh -a 4 -p 2 [testenv:py_func_v4] commands = {toxinidir}/tools/py_functional_tests.sh -a 4 -p 2