pax_global_header00006660000000000000000000000064141614134120014507gustar00rootroot0000000000000052 comment=f6c44e657fa9e929efde8d98b512b210e002be6e python-gitlab-2.10.1/000077500000000000000000000000001416141341200143515ustar00rootroot00000000000000python-gitlab-2.10.1/.commitlintrc.json000066400000000000000000000000651416141341200200270ustar00rootroot00000000000000{ "extends": ["@commitlint/config-conventional"] } python-gitlab-2.10.1/.dockerignore000066400000000000000000000000471416141341200170260ustar00rootroot00000000000000venv/ dist/ build/ *.egg-info .github/ python-gitlab-2.10.1/.github/000077500000000000000000000000001416141341200157115ustar00rootroot00000000000000python-gitlab-2.10.1/.github/ISSUE_TEMPLATE.md000066400000000000000000000003501416141341200204140ustar00rootroot00000000000000## 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.10.1/.github/workflows/000077500000000000000000000000001416141341200177465ustar00rootroot00000000000000python-gitlab-2.10.1/.github/workflows/docs.yml000066400000000000000000000015031416141341200214200ustar00rootroot00000000000000name: Docs on: push: branches: - master pull_request: branches: - master env: PY_COLORS: 1 jobs: sphinx: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: 3.9 - name: Install dependencies run: pip install tox - name: Build docs env: TOXENV: docs run: tox twine-check: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: 3.9 - name: Install dependencies run: pip install tox twine wheel - name: Check twine readme rendering env: TOXENV: twine-check run: tox python-gitlab-2.10.1/.github/workflows/lint.yml000066400000000000000000000015541416141341200214440ustar00rootroot00000000000000name: Lint on: push: branches: - master pull_request: branches: - master env: PY_COLORS: 1 jobs: commitlint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: fetch-depth: 0 - uses: wagoid/commitlint-github-action@v4 linters: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - run: pip install --upgrade tox - name: Run black code formatter (https://black.readthedocs.io/en/stable/) run: tox -e black -- --check - name: Run flake8 (https://flake8.pycqa.org/en/latest/) run: tox -e pep8 - name: Run mypy static typing checker (http://mypy-lang.org/) run: tox -e mypy - name: Run isort import order checker (https://pycqa.github.io/isort/) run: tox -e isort -- --check python-gitlab-2.10.1/.github/workflows/release.yml000066400000000000000000000010051416141341200221050ustar00rootroot00000000000000name: Release on: schedule: - cron: '0 0 28 * *' # Monthly auto-release workflow_dispatch: # Manual trigger for quick fixes jobs: release: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: fetch-depth: 0 token: ${{ secrets.RELEASE_GITHUB_TOKEN }} - name: Python Semantic Release uses: relekang/python-semantic-release@master with: github_token: ${{ secrets.RELEASE_GITHUB_TOKEN }} pypi_token: ${{ secrets.PYPI_TOKEN }} python-gitlab-2.10.1/.github/workflows/test.yml000066400000000000000000000040701416141341200214510ustar00rootroot00000000000000name: Test on: push: branches: - master pull_request: branches: - master env: PY_COLORS: 1 jobs: unit: runs-on: ubuntu-20.04 strategy: matrix: include: - python-version: 3.6 toxenv: py36 - python-version: 3.7 toxenv: py37 - python-version: 3.8 toxenv: py38 - python-version: 3.9 toxenv: py39 steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: pip install tox pytest-github-actions-annotate-failures - name: Run tests env: TOXENV: ${{ matrix.toxenv }} run: tox functional: runs-on: ubuntu-20.04 strategy: matrix: toxenv: [py_func_v4, cli_func_v4] steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: 3.9 - name: Install dependencies run: pip install tox pytest-github-actions-annotate-failures - name: Run tests env: TOXENV: ${{ matrix.toxenv }} run: tox - name: Upload codecov coverage uses: codecov/codecov-action@v2 with: files: ./coverage.xml flags: ${{ matrix.toxenv }} fail_ci_if_error: true coverage: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: 3.9 - name: Install dependencies run: pip install tox pytest-github-actions-annotate-failures - name: Run tests env: PY_COLORS: 1 TOXENV: cover run: tox - name: Upload codecov coverage uses: codecov/codecov-action@v2 with: files: ./coverage.xml flags: unit fail_ci_if_error: true python-gitlab-2.10.1/.gitlab-ci.yml000066400000000000000000000015621416141341200170110ustar00rootroot00000000000000image: python:3.9 stages: - deploy - deploy-latest 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.10.1/.pre-commit-config.yaml000066400000000000000000000013251416141341200206330ustar00rootroot00000000000000default_language_version: python: python3 repos: - repo: https://github.com/psf/black rev: 20.8b1 hooks: - id: black - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook rev: v5.0.0 hooks: - id: commitlint additional_dependencies: ['@commitlint/config-conventional'] stages: [commit-msg] - repo: https://github.com/pycqa/flake8 rev: 3.9.2 hooks: - id: flake8 - repo: https://github.com/pycqa/isort rev: 5.9.3 hooks: - id: isort - repo: https://github.com/pre-commit/mirrors-mypy rev: v0.910 hooks: - id: mypy additional_dependencies: - types-PyYAML==5.4.8 - types-requests==2.25.6 python-gitlab-2.10.1/.readthedocs.yml000066400000000000000000000002321416141341200174340ustar00rootroot00000000000000version: 2 sphinx: configuration: docs/conf.py formats: - pdf - epub python: version: 3.8 install: - requirements: requirements-docs.txt python-gitlab-2.10.1/.renovaterc.json000066400000000000000000000014511416141341200174730ustar00rootroot00000000000000{ "extends": [ "config:base" ], "pip_requirements": { "fileMatch": ["^requirements(-[\\w]*)?\\.txt$"] }, "regexManagers": [ { "fileMatch": ["^tests/functional/fixtures/.env$"], "matchStrings": ["GITLAB_TAG=(?.*?)\n"], "depNameTemplate": "gitlab/gitlab-ce", "datasourceTemplate": "docker", "versioningTemplate": "loose" }, { "fileMatch": ["^.pre-commit-config.yaml$"], "matchStrings": ["- (?.*?)==(?.*?)\n"], "datasourceTemplate": "pypi", "versioningTemplate": "pep440" } ], "packageRules": [ { "packagePatterns": ["^gitlab\/gitlab-.+$"], "automerge": true }, { "matchPackagePrefixes": ["types-"], "groupName": "typing dependencies" } ] } python-gitlab-2.10.1/AUTHORS000066400000000000000000000005451416141341200154250ustar00rootroot00000000000000Authors / Maintainers --------------------- Original creator, no longer active ================================== Gauvain Pocentek Current ======= Nejc Habjan Max Wittig Roger Meier Contributors ------------ See ``git log`` for a full list of contributors. python-gitlab-2.10.1/CHANGELOG.md000066400000000000000000000161731416141341200161720ustar00rootroot00000000000000# Changelog ## v2.10.1 (2021-08-28) ### Fix * **mixins:** Improve deprecation warning ([`57e0187`](https://github.com/python-gitlab/python-gitlab/commit/57e018772492a8522b37d438d722c643594cf580)) * **deps:** Upgrade requests to 2.25.0 (see CVE-2021-33503) ([`ce995b2`](https://github.com/python-gitlab/python-gitlab/commit/ce995b256423a0c5619e2a6c0d88e917aad315ba)) ### Documentation * **mergequests:** Gl.mergequests.list documentation was missleading ([`5b5a7bc`](https://github.com/python-gitlab/python-gitlab/commit/5b5a7bcc70a4ddd621cbd59e134e7004ad2d9ab9)) ## v2.10.0 (2021-07-28) ### Feature * **api:** Add merge_ref for merge requests ([`1e24ab2`](https://github.com/python-gitlab/python-gitlab/commit/1e24ab247cc783ae240e94f6cb379fef1e743a52)) * **api:** Add `name_regex_keep` attribute in `delete_in_bulk()` ([`e49ff3f`](https://github.com/python-gitlab/python-gitlab/commit/e49ff3f868cbab7ff81115f458840b5f6d27d96c)) ### Fix * **api:** Do not require Release name for creation ([`98cd03b`](https://github.com/python-gitlab/python-gitlab/commit/98cd03b7a3085356b5f0f4fcdb7dc729b682f481)) ### Documentation * **readme:** Move contributing docs to CONTRIBUTING.rst ([`edf49a3`](https://github.com/python-gitlab/python-gitlab/commit/edf49a3d855b1ce4e2bd8a7038b7444ff0ab5fdc)) * Add example for mr.merge_ref ([`b30b8ac`](https://github.com/python-gitlab/python-gitlab/commit/b30b8ac27d98ed0a45a13775645d77b76e828f95)) * **project:** Add example on getting a single project using name with namespace ([`ef16a97`](https://github.com/python-gitlab/python-gitlab/commit/ef16a979031a77155907f4160e4f5e159d839737)) ## v2.9.0 (2021-06-28) ### Feature * **release:** Allow to update release ([`b4c4787`](https://github.com/python-gitlab/python-gitlab/commit/b4c4787af54d9db6c1f9e61154be5db9d46de3dd)) * **api:** Add group hooks ([`4a7e9b8`](https://github.com/python-gitlab/python-gitlab/commit/4a7e9b86aa348b72925bce3af1e5d988b8ce3439)) * **api:** Remove responsibility for API inconsistencies for MR reviewers ([`3d985ee`](https://github.com/python-gitlab/python-gitlab/commit/3d985ee8cdd5d27585678f8fbb3eb549818a78eb)) * **api:** Add MR pipeline manager in favor of pipelines() method ([`954357c`](https://github.com/python-gitlab/python-gitlab/commit/954357c49963ef51945c81c41fd4345002f9fb98)) * **api:** Add support for creating/editing reviewers in project merge requests ([`676d1f6`](https://github.com/python-gitlab/python-gitlab/commit/676d1f6565617a28ee84eae20e945f23aaf3d86f)) ### Documentation * **tags:** Remove deprecated functions ([`1b1a827`](https://github.com/python-gitlab/python-gitlab/commit/1b1a827dd40b489fdacdf0a15b0e17a1a117df40)) * **release:** Add update example ([`6254a5f`](https://github.com/python-gitlab/python-gitlab/commit/6254a5ff6f43bd7d0a26dead304465adf1bd0886)) * Make Gitlab class usable for intersphinx ([`8753add`](https://github.com/python-gitlab/python-gitlab/commit/8753add72061ea01c508a42d16a27388b1d92677)) ## v2.8.0 (2021-06-10) ### Feature * Add keys endpoint ([`a81525a`](https://github.com/python-gitlab/python-gitlab/commit/a81525a2377aaed797af0706b00be7f5d8616d22)) * **objects:** Add support for Group wikis ([#1484](https://github.com/python-gitlab/python-gitlab/issues/1484)) ([`74f5e62`](https://github.com/python-gitlab/python-gitlab/commit/74f5e62ef5bfffc7ba21494d05dbead60b59ecf0)) * **objects:** Add support for generic packages API ([`79d88bd`](https://github.com/python-gitlab/python-gitlab/commit/79d88bde9e5e6c33029e4a9f26c97404e6a7a874)) * **api:** Add deployment mergerequests interface ([`fbbc0d4`](https://github.com/python-gitlab/python-gitlab/commit/fbbc0d400015d7366952a66e4401215adff709f0)) * **objects:** Support all issues statistics endpoints ([`f731707`](https://github.com/python-gitlab/python-gitlab/commit/f731707f076264ebea65afc814e4aca798970953)) * **objects:** Add support for descendant groups API ([`1b70580`](https://github.com/python-gitlab/python-gitlab/commit/1b70580020825adf2d1f8c37803bc4655a97be41)) * **objects:** Add pipeline test report support ([`ee9f96e`](https://github.com/python-gitlab/python-gitlab/commit/ee9f96e61ab5da0ecf469c21cccaafc89130a896)) * **objects:** Add support for billable members ([`fb0b083`](https://github.com/python-gitlab/python-gitlab/commit/fb0b083a0e536a6abab25c9ad377770cc4290fe9)) * Add feature to get inherited member for project/group ([`e444b39`](https://github.com/python-gitlab/python-gitlab/commit/e444b39f9423b4a4c85cdb199afbad987df026f1)) * Add code owner approval as attribute ([`fdc46ba`](https://github.com/python-gitlab/python-gitlab/commit/fdc46baca447e042d3b0a4542970f9758c62e7b7)) * Indicate that we are a typed package ([`e4421ca`](https://github.com/python-gitlab/python-gitlab/commit/e4421caafeeb0236df19fe7b9233300727e1933b)) * Add support for lists of integers to ListAttribute ([`115938b`](https://github.com/python-gitlab/python-gitlab/commit/115938b3e5adf9a2fb5ecbfb34d9c92bf788035e)) ### Fix * Catch invalid type used to initialize RESTObject ([`c7bcc25`](https://github.com/python-gitlab/python-gitlab/commit/c7bcc25a361f9df440f9c972672e5eec3b057625)) * Functional project service test ([#1500](https://github.com/python-gitlab/python-gitlab/issues/1500)) ([`093db9d`](https://github.com/python-gitlab/python-gitlab/commit/093db9d129e0a113995501755ab57a04e461c745)) * Ensure kwargs are passed appropriately for ObjectDeleteMixin ([`4e690c2`](https://github.com/python-gitlab/python-gitlab/commit/4e690c256fc091ddf1649e48dbbf0b40cc5e6b95)) * **cli:** Add missing list filter for jobs ([`b3d1c26`](https://github.com/python-gitlab/python-gitlab/commit/b3d1c267cbe6885ee41b3c688d82890bb2e27316)) * Change mr.merge() to use 'post_data' ([`cb6a3c6`](https://github.com/python-gitlab/python-gitlab/commit/cb6a3c672b9b162f7320c532410713576fbd1cdc)) * **cli:** Fix parsing CLI objects to classnames ([`4252070`](https://github.com/python-gitlab/python-gitlab/commit/42520705a97289ac895a6b110d34d6c115e45500)) * **objects:** Return server data in cancel/retry methods ([`9fed061`](https://github.com/python-gitlab/python-gitlab/commit/9fed06116bfe5df79e6ac5be86ae61017f9a2f57)) * **objects:** Add missing group attributes ([`d20ff4f`](https://github.com/python-gitlab/python-gitlab/commit/d20ff4ff7427519c8abccf53e3213e8929905441)) * **objects:** Allow lists for filters for in all objects ([`603a351`](https://github.com/python-gitlab/python-gitlab/commit/603a351c71196a7f516367fbf90519f9452f3c55)) * Iids not working as a list in projects.issues.list() ([`45f806c`](https://github.com/python-gitlab/python-gitlab/commit/45f806c7a7354592befe58a76b7e33a6d5d0fe6e)) * Add a check to ensure the MRO is correct ([`565d548`](https://github.com/python-gitlab/python-gitlab/commit/565d5488b779de19a720d7a904c6fc14c394a4b9)) ### Documentation * Fix typo in http_delete docstring ([`5226f09`](https://github.com/python-gitlab/python-gitlab/commit/5226f095c39985d04c34e7703d60814e74be96f8)) * **api:** Add behavior in local attributes when updating objects ([`38f65e8`](https://github.com/python-gitlab/python-gitlab/commit/38f65e8e9994f58bdc74fe2e0e9b971fc3edf723)) * Fail on warnings during sphinx build ([`cbd4d52`](https://github.com/python-gitlab/python-gitlab/commit/cbd4d52b11150594ec29b1ce52348c1086a778c8)) python-gitlab-2.10.1/CONTRIBUTING.rst000066400000000000000000000127001416141341200170120ustar00rootroot00000000000000Contributing ============ 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 Development workflow -------------------- Before contributing, please make sure you have `pre-commit `_ installed and configured. This will help automate adhering to code style and commit message guidelines described below: .. code-block:: bash cd python-gitlab/ pip3 install --user pre-commit pre-commit install -t pre-commit -t commit-msg --install-hooks Please provide your patches as GitHub pull requests. Thanks! Commit message guidelines ------------------------- 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. Code-Style ---------- We use black as code formatter, so you'll need to format your changes using the `black code formatter `_. Pre-commit hooks will validate/format your code when committing. You can then stage any changes ``black`` added if the commit failed. To format your code according to our guidelines before committing, run: .. code-block:: bash cd python-gitlab/ pip3 install --user black black . Running unit tests ------------------ Before submitting a pull request make sure that the tests and lint checks still succeed with your change. Unit tests and functional tests run in GitHub Actions and passing checks are mandatory to get merge requests accepted. Please write new unit tests with pytest and using `responses `_. An example can be found in ``tests/unit/objects/test_runner.py`` You need to install ``tox`` (``pip3 install tox``) to run tests and lint checks locally: .. code-block:: bash # run unit tests using your installed python3, and all lint checks: tox -s # run unit tests for all supported python3 versions, and all lint checks: tox # run tests in one environment only: tox -epy38 # build the documentation, the result will be generated in # build/sphinx/html/ tox -edocs Running integration tests ------------------------- Integration tests run 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: tox -e cli_func_v4 # run the python API tests: tox -e py_func_v4 When developing tests it can be a little frustrating to wait for GitLab to spin up every run. To prevent the containers from being cleaned up afterwards, pass `--keep-containers` to pytest, i.e.: .. code-block:: bash tox -e py_func_v4 -- --keep-containers If you then wish to test against a clean slate, you may perform a manual clean up of the containers by running: .. code-block:: bash docker-compose -f tests/functional/fixtures/docker-compose.yml -p pytest-python-gitlab down -v By default, the tests run against the latest version of the ``gitlab/gitlab-ce`` image. You can override both the image and tag by providing either the ``GITLAB_IMAGE`` or ``GITLAB_TAG`` environment variables. This way you can run tests against different versions, such as ``nightly`` for features in an upcoming release, or an older release (e.g. ``12.8.0-ce.0``). The tag must match an exact tag on Docker Hub: .. code-block:: bash # run tests against `nightly` or specific tag GITLAB_TAG=nightly tox -e py_func_v4 GITLAB_TAG=12.8.0-ce.0 tox -e py_func_v4 # run tests against the latest gitlab EE image GITLAB_IMAGE=gitlab/gitlab-ee tox -e py_func_v4 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 docker rm -f gitlab-runner-test Releases -------- A release is automatically published once a month on the 28th if any commits merged to the main branch contain commit message types that signal a semantic version bump (``fix``, ``feat``, ``BREAKING CHANGE:``). Additionally, the release workflow can be run manually by maintainers to publish urgent fixes, either on GitHub or using the ``gh`` CLI with ``gh workflow run release.yml``. **Note:** As a maintainer, this means you should carefully review commit messages used by contributors in their pull requests. If scopes such as ``fix`` and ``feat`` are applied to trivial commits not relevant to end users, it's best to squash their pull requests and summarize the addition in a single conventional commit. This avoids triggering incorrect version bumps and releases without functional changes. The release workflow uses `python-semantic-release `_ and does the following: * Bumps the version in ``__version__.py`` and adds an entry in ``CHANGELOG.md``, * Commits and tags the changes, then pushes to the main branch as the ``github-actions`` user, * Creates a release from the tag and adds the changelog entry to the release notes, * Uploads the package as assets to the GitHub release, * Uploads the package to PyPI using ``PYPI_TOKEN`` (configured as a secret). python-gitlab-2.10.1/COPYING000066400000000000000000000167431416141341200154170ustar00rootroot00000000000000 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.10.1/ChangeLog.rst000066400000000000000000000700741416141341200167420ustar00rootroot00000000000000ChangeLog - 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.10.1/Dockerfile000066400000000000000000000006001416141341200163370ustar00rootroot00000000000000FROM python:3.9-alpine AS build WORKDIR /opt/python-gitlab COPY . . RUN python setup.py bdist_wheel FROM python:3.9-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.10.1/MANIFEST.in000066400000000000000000000002661416141341200161130ustar00rootroot00000000000000include COPYING AUTHORS ChangeLog.rst RELEASE_NOTES.rst requirements*.txt include tox.ini recursive-include tests * recursive-include docs *j2 *.py *.rst api/*.rst Makefile make.bat python-gitlab-2.10.1/README.rst000066400000000000000000000055211416141341200160430ustar00rootroot00000000000000.. image:: https://github.com/python-gitlab/python-gitlab/workflows/Test/badge.svg :target: https://github.com/python-gitlab/python-gitlab/actions .. 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://codecov.io/github/python-gitlab/python-gitlab/coverage.svg?branch=master :target: https://codecov.io/github/python-gitlab/python-gitlab?branch=master .. 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. Gitter Community Chat ===================== There is a `gitter `_ community chat available at https://gitter.im/python-gitlab/Lobby 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 ============ For guidelines for contributing to ``python-gitlab``, refer to `CONTRIBUTING.rst `_. python-gitlab-2.10.1/RELEASE_NOTES.rst000066400000000000000000000175201416141341200171400ustar00rootroot00000000000000############# 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.10.1/codecov.yml000066400000000000000000000003251416141341200165160ustar00rootroot00000000000000codecov: require_ci_to_pass: yes coverage: precision: 2 round: down range: "70...100" comment: layout: "diff,flags,files" behavior: default require_changes: yes github_checks: annotations: true python-gitlab-2.10.1/docker-entrypoint.sh000077500000000000000000000010771416141341200203750ustar00rootroot00000000000000#!/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.10.1/docs/000077500000000000000000000000001416141341200153015ustar00rootroot00000000000000python-gitlab-2.10.1/docs/Makefile000066400000000000000000000152061416141341200167450ustar00rootroot00000000000000# 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.10.1/docs/__init__.py000066400000000000000000000000001416141341200174000ustar00rootroot00000000000000python-gitlab-2.10.1/docs/_templates/000077500000000000000000000000001416141341200174365ustar00rootroot00000000000000python-gitlab-2.10.1/docs/_templates/breadcrumbs.html000066400000000000000000000017321416141341200226200ustar00rootroot00000000000000{# 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.10.1/docs/api-objects.rst000066400000000000000000000024471416141341200202420ustar00rootroot00000000000000############ API examples ############ .. toctree:: :maxdepth: 1 gl_objects/access_requests gl_objects/appearance gl_objects/applications gl_objects/emojis gl_objects/badges gl_objects/branches gl_objects/clusters gl_objects/messages gl_objects/commits gl_objects/deploy_keys gl_objects/deploy_tokens 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/keys 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/packages gl_objects/pagesdomains gl_objects/personal_access_tokens gl_objects/pipelines_and_jobs gl_objects/projects gl_objects/project_access_tokens gl_objects/protected_branches gl_objects/releases gl_objects/runners gl_objects/remote_mirrors 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/variables gl_objects/sidekiq gl_objects/wikis python-gitlab-2.10.1/docs/api-usage.rst000066400000000000000000000315131416141341200177110ustar00rootroot00000000000000############################ 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') # Define your own custom user agent for requests gl = gitlab.Gitlab('http://10.0.0.1', user_agent='my-package/1.0.0') # 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: https://2.python-requests.org/en/master/user/advanced/#proxies SSL certificate verification ---------------------------- python-gitlab relies on the CA certificate bundle in the `certifi` package that comes with the requests library. If you need python-gitlab to use your system CA store instead, you can provide the path to the CA bundle in the `REQUESTS_CA_BUNDLE` environment variable. Reference: https://2.python-requests.org/en/master/user/advanced/#ssl-cert-verification 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: https://2.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) .. _object_attributes: Attributes in updated objects ============================= When methods manipulate an existing object, such as with ``refresh()`` and ``save()``, the object will only have attributes that were returned by the server. In some cases, such as when the initial request fetches attributes that are needed later for additional processing, this may not be desired: .. code-block:: python project = gl.projects.get(1, statistics=True) project.statistics project.refresh() project.statistics # AttributeError To avoid this, either copy the object/attributes before calling ``refresh()``/``save()`` or subsequently perform another ``get()`` call as needed, to fetch the attributes you want. python-gitlab-2.10.1/docs/api/000077500000000000000000000000001416141341200160525ustar00rootroot00000000000000python-gitlab-2.10.1/docs/api/gitlab.rst000066400000000000000000000025041416141341200200470ustar00rootroot00000000000000API reference (``gitlab`` package) ================================== Module contents --------------- .. automodule:: gitlab :members: :undoc-members: :show-inheritance: .. autoclass:: gitlab.Gitlab :members: :undoc-members: :show-inheritance: .. autoclass:: gitlab.GitlabList :members: :undoc-members: :show-inheritance: 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: python-gitlab-2.10.1/docs/api/gitlab.v4.rst000066400000000000000000000005001416141341200203710ustar00rootroot00000000000000gitlab.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.10.1/docs/changelog.rst000066400000000000000000000000361416141341200177610ustar00rootroot00000000000000.. include:: ../ChangeLog.rst python-gitlab-2.10.1/docs/cli-objects.rst000066400000000000000000000012031416141341200202250ustar00rootroot00000000000000################################## CLI reference (``gitlab`` command) ################################## .. warning:: The following is a complete, auto-generated list of subcommands available via the :command:`gitlab` command-line tool. Some of the actions may currently not work as expected or lack functionality available via the API. Please see the existing `list of CLI related issues`_, or open a new one if it is not already listed there. .. _list of CLI related issues: https://github.com/python-gitlab/python-gitlab/issues?q=is%3Aopen+is%3Aissue+label%3Acli .. autoprogram:: gitlab.cli:docs() :prog: gitlab python-gitlab-2.10.1/docs/cli-usage.rst000066400000000000000000000275701416141341200177170ustar00rootroot00000000000000#################### ``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 3 configuration files by default: ``PYTHON_GITLAB_CFG`` environment variable An environment variable that contains the path to a configuration file ``/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 = helper: path/to/helper.sh 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. * - ``user_agent`` - ``str`` - A string defining a custom user agent to use when ``gitlab`` makes requests. 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. We recommend that you use `Credential helpers`_ to securely store your tokens. .. list-table:: GitLab server options :header-rows: 1 * - Option - Description * - ``url`` - URL for the GitLab server * - ``private_token`` - Your user token. Login/password is not supported. Refer to `the official documentation `__ to learn how to obtain a token. * - ``oauth_token`` - An Oauth token for authentication. The Gitlab server must be configured to support this authentication method. * - ``job_token`` - Your job token. See `the official documentation `__ 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 Credential helpers ------------------ For all configuration options that contain secrets (``http_password``, ``personal_token``, ``oauth_token``, ``job_token``), you can specify a helper program to retrieve the secret indicated by a ``helper:`` prefix. This allows you to fetch values from a local keyring store or cloud-hosted vaults such as Bitwarden. Environment variables are expanded if they exist and ``~`` expands to your home directory. It is expected that the helper program prints the secret to standard output. To use shell features such as piping to retrieve the value, you will need to use a wrapper script; see below. Example for a `keyring `_ helper: .. code-block:: ini [global] default = somewhere ssl_verify = true timeout = 5 [somewhere] url = http://somewhe.re private_token = helper: keyring get Service Username timeout = 1 Example for a `pass `_ helper with a wrapper script: .. code-block:: ini [global] default = somewhere ssl_verify = true timeout = 5 [somewhere] url = http://somewhe.re private_token = helper: /path/to/helper.sh timeout = 1 In `/path/to/helper.sh`: .. code-block:: bash #!/bin/bash pass show path/to/password | head -n 1 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 ======== **Notice:** For a complete list of objects and actions available, see :doc:`/cli-objects`. List the projects (paginated): .. code-block:: console $ gitlab project list List all the projects: .. code-block:: console $ gitlab project list --all List all projects of a group: .. code-block:: console $ gitlab group-project list --all --group-id 1 List all projects of a group and its subgroups: .. code-block:: console $ gitlab group-project list --all --include-subgroups true --group-id 1 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 Create a deploy token for a project: .. code-block:: console $ gitlab -v project-deploy-token create --project-id 2 \ --name bar --username root --expires-at "2021-09-09" --scopes "read_repository" List deploy tokens for a group: .. code-block:: console $ gitlab -v group-deploy-token list --group-id 3 List packages for a project: .. code-block:: console $ gitlab -v project-package list --project-id 3 List packages for a group: .. code-block:: console $ gitlab -v group-package list --group-id 3 Get a specific project package by id: .. code-block:: console $ gitlab -v project-package get --id 1 --project-id 3 Delete a specific project package by id: .. code-block:: console $ gitlab -v project-package delete --id 1 --project-id 3 Upload a generic package to a project: .. code-block:: console $ gitlab generic-package upload --project-id 1 --package-name hello-world \ --package-version v1.0.0 --file-name hello.tar.gz --path /path/to/hello.tar.gz Download a project's generic package: .. code-block:: console $ gitlab generic-package download --project-id 1 --package-name hello-world \ --package-version v1.0.0 --file-name hello.tar.gz > /path/to/hello.tar.gz Get a list of issues 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" Get a specific project commit by its SHA id: .. code-block:: console $ gitlab project-commit get --project-id 2 --id a43290c Get the signature (e.g. GPG or x509) of a signed commit: .. code-block:: console $ gitlab project-commit signature --project-id 2 --id a43290c 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" Download the artifacts zip archive of a job: .. code-block:: console $ gitlab project-job artifacts --id 10 --project-id 1 > artifacts.zip 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.10.1/docs/conf.py000066400000000000000000000217241416141341200166060ustar00rootroot00000000000000#!/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 sys.path.append("../") sys.path.append(os.path.dirname(__file__)) import gitlab # noqa: E402. Needed purely for readthedocs' build 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", "sphinxcontrib.autoprogram", ] # 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.10.1/docs/ext/000077500000000000000000000000001416141341200161015ustar00rootroot00000000000000python-gitlab-2.10.1/docs/ext/__init__.py000066400000000000000000000000001416141341200202000ustar00rootroot00000000000000python-gitlab-2.10.1/docs/ext/docstrings.py000066400000000000000000000033551416141341200206400ustar00rootroot00000000000000import inspect 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 "" 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.10.1/docs/ext/manager_tmpl.j2000066400000000000000000000014601416141341200210050ustar00rootroot00000000000000{% 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.10.1/docs/faq.rst000066400000000000000000000027351416141341200166110ustar00rootroot00000000000000### 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]) I get an ``AttributeError`` when accessing attributes after ``save()`` or ``refresh()``. You are most likely trying to access an attribute that was not returned by the server on the second request. Please look at the documentation in :ref:`object_attributes` to see how to avoid this. python-gitlab-2.10.1/docs/gl_objects/000077500000000000000000000000001416141341200174145ustar00rootroot00000000000000python-gitlab-2.10.1/docs/gl_objects/access_requests.rst000066400000000000000000000026151416141341200233460ustar00rootroot00000000000000############### 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.10.1/docs/gl_objects/appearance.rst000066400000000000000000000006631416141341200222520ustar00rootroot00000000000000########## 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.10.1/docs/gl_objects/applications.rst000066400000000000000000000011221416141341200226300ustar00rootroot00000000000000############ Applications ############ Reference --------- * v4 API: + :class:`gitlab.v4.objects.Applications` + :class:`gitlab.v4.objects.ApplicationManager` + :attr:`gitlab.Gitlab.applications` * GitLab API: https://docs.gitlab.com/ce/api/applications.html Examples -------- List all OAuth applications:: applications = gl.applications.list() Create an application:: gl.applications.create({'name': 'your_app', 'redirect_uri': 'http://application.url', 'scopes': ['api']}) Delete an applications:: gl.applications.delete(app_id) # or application.delete() python-gitlab-2.10.1/docs/gl_objects/badges.rst000066400000000000000000000020321416141341200213700ustar00rootroot00000000000000###### 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 a 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.10.1/docs/gl_objects/boards.rst000066400000000000000000000045511416141341200214250ustar00rootroot00000000000000############ 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.10.1/docs/gl_objects/branches.rst000066400000000000000000000022031416141341200217300ustar00rootroot00000000000000######## 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.10.1/docs/gl_objects/clusters.rst000066400000000000000000000033751416141341200220220ustar00rootroot00000000000000############ 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.10.1/docs/gl_objects/commits.rst000066400000000000000000000066321416141341200216300ustar00rootroot00000000000000####### 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') Revert a commit on a given branch:: commit.revert(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 Get the signature of the commit (if the commit was signed, e.g. with GPG or x509):: commit.signature() 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.10.1/docs/gl_objects/deploy_keys.rst000066400000000000000000000023301416141341200224730ustar00rootroot00000000000000########### 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.10.1/docs/gl_objects/deploy_tokens.rst000066400000000000000000000074571416141341200230420ustar00rootroot00000000000000############# Deploy tokens ############# Deploy tokens allow read-only access to your repository and registry images without having a user and a password. Deploy tokens ============= This endpoint requires admin access. Reference --------- * v4 API: + :class:`gitlab.v4.objects.DeployToken` + :class:`gitlab.v4.objects.DeployTokenManager` + :attr:`gitlab.Gitlab.deploytokens` * GitLab API: https://docs.gitlab.com/ce/api/deploy_tokens.html Examples -------- Use the ``list()`` method to list all deploy tokens across the GitLab instance. :: # List deploy tokens deploy_tokens = gl.deploytokens.list() Project deploy tokens ===================== This endpoint requires project maintainer access or higher. Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectDeployToken` + :class:`gitlab.v4.objects.ProjectDeployTokenManager` + :attr:`gitlab.v4.objects.Project.deploytokens` * GitLab API: https://docs.gitlab.com/ce/api/deploy_tokens.html#project-deploy-tokens Examples -------- List the deploy tokens for a project:: deploy_tokens = project.deploytokens.list() Create a new deploy token to access registry images of a project: In addition to required parameters ``name`` and ``scopes``, this method accepts the following parameters: * ``expires_at`` Expiration date of the deploy token. Does not expire if no value is provided. * ``username`` Username for deploy token. Default is ``gitlab+deploy-token-{n}`` :: deploy_token = project.deploytokens.create({'name': 'token1', 'scopes': ['read_registry'], 'username':'', 'expires_at':''}) # show its id print(deploy_token.id) # show the token value. Make sure you save it, you won't be able to access it again. print(deploy_token.token) .. warning:: With GitLab 12.9, even though ``username`` and ``expires_at`` are not required, they always have to be passed to the API. You can set them to empty strings, see: https://gitlab.com/gitlab-org/gitlab/-/issues/211878. Also, the ``username``'s value is ignored by the API and will be overriden with ``gitlab+deploy-token-{n}``, see: https://gitlab.com/gitlab-org/gitlab/-/issues/211963 These issues were fixed in GitLab 12.10. Remove a deploy token from the project:: deploy_token.delete() # or project.deploytokens.delete(deploy_token.id) Group deploy tokens =================== Reference --------- * v4 API: + :class:`gitlab.v4.objects.GroupDeployToken` + :class:`gitlab.v4.objects.GroupDeployTokenManager` + :attr:`gitlab.v4.objects.Group.deploytokens` * GitLab API: https://docs.gitlab.com/ce/api/deploy_tokens.html#group-deploy-tokens Examples -------- List the deploy tokens for a group:: deploy_tokens = group.deploytokens.list() Create a new deploy token to access all repositories of all projects in a group: In addition to required parameters ``name`` and ``scopes``, this method accepts the following parameters: * ``expires_at`` Expiration date of the deploy token. Does not expire if no value is provided. * ``username`` Username for deploy token. Default is ``gitlab+deploy-token-{n}`` :: deploy_token = group.deploytokens.create({'name': 'token1', 'scopes': ['read_repository'], 'username':'', 'expires_at':''}) # show its id print(deploy_token.id) .. warning:: With GitLab 12.9, even though ``username`` and ``expires_at`` are not required, they always have to be passed to the API. You can set them to empty strings, see: https://gitlab.com/gitlab-org/gitlab/-/issues/211878. Also, the ``username``'s value is ignored by the API and will be overriden with ``gitlab+deploy-token-{n}``, see: https://gitlab.com/gitlab-org/gitlab/-/issues/211963 These issues were fixed in GitLab 12.10. Remove a deploy token from the group:: deploy_token.delete() # or group.deploytokens.delete(deploy_token.id) python-gitlab-2.10.1/docs/gl_objects/deployments.rst000066400000000000000000000026031416141341200225120ustar00rootroot00000000000000########### 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() Merge requests associated with a deployment =========================================== Reference ---------- * v4 API: + :class:`gitlab.v4.objects.ProjectDeploymentMergeRequest` + :class:`gitlab.v4.objects.ProjectDeploymentMergeRequestManager` + :attr:`gitlab.v4.objects.ProjectDeployment.mergerequests` * GitLab API: https://docs.gitlab.com/ee/api/deployments.html#list-of-merge-requests-associated-with-a-deployment Examples -------- List the merge requests associated with a deployment:: deployment = project.deployments.get(42, lazy=True) mrs = deployment.mergerequests.list() python-gitlab-2.10.1/docs/gl_objects/discussions.rst000066400000000000000000000065461416141341200225270ustar00rootroot00000000000000########### 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.10.1/docs/gl_objects/emojis.rst000066400000000000000000000022541416141341200214370ustar00rootroot00000000000000############ 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.10.1/docs/gl_objects/environments.rst000066400000000000000000000016071416141341200227010ustar00rootroot00000000000000############ 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.10.1/docs/gl_objects/epics.rst000066400000000000000000000024261416141341200212550ustar00rootroot00000000000000##### 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.10.1/docs/gl_objects/events.rst000066400000000000000000000042361416141341200214570ustar00rootroot00000000000000###### Events ###### 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() Resource state events ===================== Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectIssueResourceStateEvent` + :class:`gitlab.v4.objects.ProjectIssueResourceStateEventManager` + :attr:`gitlab.v4.objects.ProjectIssue.resourcestateevents` + :class:`gitlab.v4.objects.ProjectMergeRequestResourceStateEvent` + :class:`gitlab.v4.objects.ProjectMergeRequestResourceStateEventManager` + :attr:`gitlab.v4.objects.ProjectMergeRequest.resourcestateevents` * GitLab API: https://docs.gitlab.com/ee/api/resource_state_events.html Examples -------- You can list and get specific resource state events (via their id) for project issues and project merge requests. List the state events of a project issue (paginated):: state_events = issue.resourcestateevents.list() Get a specific state event of a project issue by its id:: state_event = issue.resourcestateevents.get(1) List the state events of a project merge request (paginated):: state_events = mr.resourcestateevents.list() Get a specific state event of a project merge request by its id:: state_event = mr.resourcestateevents.get(1) python-gitlab-2.10.1/docs/gl_objects/features.rst000066400000000000000000000011611416141341200217630ustar00rootroot00000000000000############## 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) feature = gl.features.set(feature_name, True, user=filipowm) feature = gl.features.set(feature_name, 40, group=mygroup) Delete a feature:: feature.delete() python-gitlab-2.10.1/docs/gl_objects/geo_nodes.rst000066400000000000000000000013161416141341200221110ustar00rootroot00000000000000######### 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.10.1/docs/gl_objects/groups.rst000066400000000000000000000212471416141341200214730ustar00rootroot00000000000000###### 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'}) Create a subgroup under an existing group:: subgroup = gl.groups.create({'name': 'subgroup1', 'path': 'subgroup1', 'parent_id': parent_group_id}) Update a group:: group.description = 'My awesome group' group.save() Set the avatar image for a group:: # the avatar image can be passed as data (content of the file) or as a file # object opened in binary mode group.avatar = open('path/to/file.png', 'rb') group.save() Remove a group:: gl.groups.delete(group_id) # or group.delete() Share/unshare the group with a group:: group.share(group2.id, gitlab.DEVELOPER_ACCESS) group.unshare(group2.id) Import / Export =============== You can export groups from gitlab, and re-import them to create new groups. Reference --------- * v4 API: + :class:`gitlab.v4.objects.GroupExport` + :class:`gitlab.v4.objects.GroupExportManager` + :attr:`gitlab.v4.objects.Group.exports` + :class:`gitlab.v4.objects.GroupImport` + :class:`gitlab.v4.objects.GroupImportManager` + :attr:`gitlab.v4.objects.Group.imports` + :attr:`gitlab.v4.objects.GroupManager.import_group` * GitLab API: https://docs.gitlab.com/ce/api/group_import_export.html Examples -------- A group 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 .. warning:: Unlike the Project Export API, GitLab does not provide an export_status for Group Exports. It is up to the user to ensure the export is finished. However, Group Exports only contain metadata, so they are much faster than Project Exports. :: # Create the export group = gl.groups.get(my_group) export = group.exports.create() # Wait for the export to finish time.sleep(3) # Download the result with open('/tmp/export.tgz', 'wb') as f: export.download(streamed=True, action=f.write) Import the group:: with open('/tmp/export.tgz', 'rb') as f: gl.groups.import_group(f, path='imported-group', name="Imported Group") 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() Descendant Groups ================= Reference --------- * v4 API: + :class:`gitlab.v4.objects.GroupDescendantGroup` + :class:`gitlab.v4.objects.GroupDescendantGroupManager` + :attr:`gitlab.v4.objects.Group.descendant_groups` Examples -------- List the descendant groups of a group:: descendant_groups = group.descendant_groups.list() .. note:: Like the ``GroupSubgroup`` objects described above, ``GroupDescendantGroup`` objects do not expose the same API as the ``Group`` objects. Create a new ``Group`` object instead if needed, as shown in the subgroup example. 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` + :class:`gitlab.v4.objects.GroupMemberAllManager` + :class:`gitlab.v4.objects.GroupBillableMember` + :class:`gitlab.v4.objects.GroupBillableMemberManager` + :attr:`gitlab.v4.objects.Group.members` + :attr:`gitlab.v4.objects.Group.members_all` + :attr:`gitlab.v4.objects.Group.billable_members` * GitLab API: https://docs.gitlab.com/ce/api/members.html Billable group members are only available in GitLab EE. Examples -------- List only direct group members:: members = group.members.list() List the group members recursively (including inherited members through ancestor groups):: members = group.members_all.list(all=True) # or members = group.members.all(all=True) # Deprecated Get only direct group member:: members = group.members.get(member_id) Get a member of a group, including members inherited through ancestor groups:: members = group.members_all.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() List billable members of a group (top-level groups only):: billable_members = group.billable_members.list() Remove a billable member from the group:: group.billable_members.delete(member_id) # or billable_member.delete() List memberships of a billable member:: billable_member.memberships.list() 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') Groups hooks ============ Reference --------- * v4 API: + :class:`gitlab.v4.objects.GroupHook` + :class:`gitlab.v4.objects.GroupHookManager` + :attr:`gitlab.v4.objects.Group.hooks` * GitLab API: https://docs.gitlab.com/ce/api/groups.html#hooks Examples -------- List the group hooks:: hooks = group.hooks.list() Get a group hook:: hook = group.hooks.get(hook_id) Create a group hook:: hook = group.hooks.create({'url': 'http://my/action/url', 'push_events': 1}) Update a group hook:: hook.push_events = 0 hook.save() Delete a group hook:: group.hooks.delete(hook_id) # or hook.delete() python-gitlab-2.10.1/docs/gl_objects/issues.rst000066400000000000000000000144511416141341200214660ustar00rootroot00000000000000.. _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 (admin or project owner only):: 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) Issues statistics ========================= Reference --------- * v4 API: + :class:`gitlab.v4.objects.IssuesStatistics` + :class:`gitlab.v4.objects.IssuesStatisticsManager` + :attr:`gitlab.issues_statistics` + :class:`gitlab.v4.objects.GroupIssuesStatistics` + :class:`gitlab.v4.objects.GroupIssuesStatisticsManager` + :attr:`gitlab.v4.objects.Group.issues_statistics` + :class:`gitlab.v4.objects.ProjectIssuesStatistics` + :class:`gitlab.v4.objects.ProjectIssuesStatisticsManager` + :attr:`gitlab.v4.objects.Project.issues_statistics` * GitLab API: https://docs.gitlab.com/ce/api/issues_statistics.htm Examples --------- Get statistics of all issues created by the current user:: statistics = gl.issues_statistics.get() Get statistics of all issues the user has access to:: statistics = gl.issues_statistics.get(scope='all') Get statistics of issues for the user with ``foobar`` in the ``title`` or the ``description``:: statistics = gl.issues_statistics.get(search='foobar') Get statistics of all issues in a group:: statistics = group.issues_statistics.get() Get statistics of issues in a group with ``foobar`` in the ``title`` or the ``description``:: statistics = group.issues_statistics.get(search='foobar') Get statistics of all issues in a project:: statistics = project.issues_statistics.get() Get statistics of issues in a project with ``foobar`` in the ``title`` or the ``description``:: statistics = project.issues_statistics.get(search='foobar') python-gitlab-2.10.1/docs/gl_objects/keys.rst000066400000000000000000000007521416141341200211250ustar00rootroot00000000000000#### Keys #### Keys ==== Reference --------- * v4 API + :class:`gitlab.v4.objects.Key` + :class:`gitlab.v4.objects.KeyManager` + :attr:`gitlab.Gitlab.keys` * GitLab API: https://docs.gitlab.com/ce/api/keys.html Examples -------- Get an ssh key by its id (requires admin access):: key = gl.keys.get(key_id) Get an ssh key (requires admin access) or a deploy key by its fingerprint:: key = gl.keys.get(fingerprint="SHA256:ERJJ/OweAM6jA8OjJ/gXs4N5fqUaREEJnz/EyfywfXY") python-gitlab-2.10.1/docs/gl_objects/labels.rst000066400000000000000000000042631416141341200214150ustar00rootroot00000000000000###### 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.10.1/docs/gl_objects/messages.rst000066400000000000000000000017341416141341200217620ustar00rootroot00000000000000################## 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.10.1/docs/gl_objects/milestones.rst000066400000000000000000000050671416141341200223400ustar00rootroot00000000000000########## Milestones ########## Project 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() Milestone events ================ Resource milestone events keep track of what happens to GitLab issues and merge requests. Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectIssueResourceMilestoneEvent` + :class:`gitlab.v4.objects.ProjectIssueResourceMilestoneEventManager` + :attr:`gitlab.v4.objects.ProjectIssue.resourcemilestoneevents` + :class:`gitlab.v4.objects.ProjectMergeRequestResourceMilestoneEvent` + :class:`gitlab.v4.objects.ProjectMergeRequestResourceMilestoneEventManager` + :attr:`gitlab.v4.objects.ProjectMergeRequest.resourcemilestoneevents` * GitLab API: https://docs.gitlab.com/ee/api/resource_milestone_events.html Examples -------- Get milestones for a resource (issue, merge request):: milestones = resource.resourcemilestoneevents.list() Get a specific milestone for a resource:: milestone = resource.resourcemilestoneevents.get(milestone_id) python-gitlab-2.10.1/docs/gl_objects/mr_approvals.rst000066400000000000000000000046201416141341200226550ustar00rootroot00000000000000################################ 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` + :class:`gitlab.v4.objects.ProjectMergeRequestApprovalRule` + :class:`gitlab.v4.objects.ProjectMergeRequestApprovalRuleManager` + :attr:`gitlab.v4.objects.ProjectMergeRequest.approval_rules` * 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.set_approvers(approvals_required = 1) Change project-level MR allowed approvers:: project.approvals.set_approvers(approver_ids=[105], approver_group_ids=[653, 654]) Create a new MR-level approval rule or change an existing MR-level approval rule:: mr.approvals.set_approvers(approvals_required = 1, approver_ids=[105], approver_group_ids=[653, 654], approval_rule_name="my MR custom approval rule") List MR-level MR approval rules:: mr.approval_rules.list() Change MR-level MR approval rule:: mr_approvalrule.user_ids = [105] mr_approvalrule.approvals_required = 2 mr_approvalrule.group_ids = [653, 654] mr_approvalrule.save() Create a MR-level MR approval rule:: mr.approval_rules.create({ "name": "my MR custom approval rule", "approvals_required": 2, "rule_type": "regular", "user_ids": [105], "group_ids": [653, 654], }) python-gitlab-2.10.1/docs/gl_objects/mrs.rst000066400000000000000000000112361416141341200207520ustar00rootroot00000000000000.. _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 created by the user of the token on the GitLab server:: mrs = gl.mergerequests.list() List the merge requests available on the GitLab server:: mrs = gl.mergerequests.list(scope="all") 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 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() Attempt to merge changes between source and target branch:: response = mr.merge_ref() print(response['commit_id']) Merge Request Pipelines ======================= Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectMergeRequestPipeline` + :class:`gitlab.v4.objects.ProjectMergeRequestPipelineManager` + :attr:`gitlab.v4.objects.ProjectMergeRequest.pipelines` * GitLab API: https://docs.gitlab.com/ee/api/merge_requests.html#list-mr-pipelines Examples -------- List pipelines for a merge request:: pipelines = mr.pipelines.list() Create a pipeline for a merge request:: pipeline = mr.pipelines.create() python-gitlab-2.10.1/docs/gl_objects/namespaces.rst000066400000000000000000000006231416141341200222660ustar00rootroot00000000000000########## 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.10.1/docs/gl_objects/notes.rst000066400000000000000000000025461416141341200213050ustar00rootroot00000000000000.. _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.10.1/docs/gl_objects/notifications.rst000066400000000000000000000034551416141341200230260ustar00rootroot00000000000000##################### 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.10.1/docs/gl_objects/packages.rst000066400000000000000000000055071416141341200217330ustar00rootroot00000000000000######## Packages ######## Packages allow you to utilize GitLab as a private repository for a variety of common package managers, as well as GitLab's generic package registry. Project Packages ===================== Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectPackage` + :class:`gitlab.v4.objects.ProjectPackageManager` + :attr:`gitlab.v4.objects.Project.packages` * GitLab API: https://docs.gitlab.com/ee/api/packages.html#within-a-project Examples -------- List the packages in a project:: packages = project.packages.list() Filter the results by ``package_type`` or ``package_name`` :: packages = project.packages.list(package_type='pypi') Get a specific package of a project by id:: package = project.packages.get(1) Delete a package from a project:: package.delete() # or project.packages.delete(package.id) Group Packages =================== Reference --------- * v4 API: + :class:`gitlab.v4.objects.GroupPackage` + :class:`gitlab.v4.objects.GroupPackageManager` + :attr:`gitlab.v4.objects.Group.packages` * GitLab API: https://docs.gitlab.com/ee/api/packages.html#within-a-group Examples -------- List the packages in a group:: packages = group.packages.list() Filter the results by ``package_type`` or ``package_name`` :: packages = group.packages.list(package_type='pypi') Project Package Files ===================== Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectPackageFile` + :class:`gitlab.v4.objects.ProjectPackageFileManager` + :attr:`gitlab.v4.objects.ProjectPackage.package_files` * GitLab API: https://docs.gitlab.com/ee/api/packages.html#list-package-files Examples -------- List package files for package in project:: package = project.packages.get(1) package_files = package.package_files.list() Generic Packages ================ You can use python-gitlab to upload and download generic packages. Reference --------- * v4 API: + :class:`gitlab.v4.objects.GenericPackage` + :class:`gitlab.v4.objects.GenericPackageManager` + :attr:`gitlab.v4.objects.Project.generic_packages` * GitLab API: https://docs.gitlab.com/ee/user/packages/generic_packages Examples -------- Upload a generic package to a project:: project = gl.projects.get(1, lazy=True) package = project.generic_packages.upload( package_name="hello-world", package_version="v1.0.0", file_name="hello.tar.gz", path="/path/to/local/hello.tar.gz" ) Download a project's generic package:: project = gl.projects.get(1, lazy=True) package = project.generic_packages.download( package_name="hello-world", package_version="v1.0.0", file_name="hello.tar.gz", ) .. hint:: You can use the Packages API described above to find packages and retrieve the metadata you need download them. python-gitlab-2.10.1/docs/gl_objects/pagesdomains.rst000066400000000000000000000023301416141341200226160ustar00rootroot00000000000000############# 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.10.1/docs/gl_objects/personal_access_tokens.rst000066400000000000000000000011751416141341200247010ustar00rootroot00000000000000###################### Personal Access Tokens ###################### Get a list of personal access tokens References ---------- * v4 API: + :class:`gitlab.v4.objects.PersonalAccessToken` + :class:`gitlab.v4.objects.PersonalAcessTokenManager` + :attr:`gitlab.Gitlab.personal_access_tokens` * GitLab API: https://docs.gitlab.com/ee/api/personal_access_tokens.html Examples -------- List personal access tokens:: access_tokens = gl.personal_access_tokens.list() print(access_tokens[0].name) List personal access tokens from other user_id (admin only):: access_tokens = gl.personal_access_tokens.list(user_id=25) python-gitlab-2.10.1/docs/gl_objects/pipelines_and_jobs.rst000066400000000000000000000174601416141341200240050ustar00rootroot00000000000000################## 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() Trigger a pipeline schedule immediately:: sched = projects.pipelineschedules.get(schedule_id) sched.play() 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() 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() Get the artifacts of a job by its name from the latest successful pipeline of a branch or tag: project.artifacts(ref_name='master', job='build') .. 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:: with open("archive.zip", "wb") as f: build_or_job.artifacts(streamed=True, action=f.write) 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() Pipeline bridges ===================== Get a list of bridge jobs (including child pipelines) for a pipeline. Reference --------- * v4 API + :class:`gitlab.v4.objects.ProjectPipelineBridge` + :class:`gitlab.v4.objects.ProjectPipelineBridgeManager` + :attr:`gitlab.v4.objects.ProjectPipeline.bridges` * GitLab API: https://docs.gitlab.com/ee/api/jobs.html#list-pipeline-bridges Examples -------- List bridges for the pipeline:: bridges = pipeline.bridges.list() Pipeline test report ==================== Get a pipeline's complete test report. Reference --------- * v4 API + :class:`gitlab.v4.objects.ProjectPipelineTestReport` + :class:`gitlab.v4.objects.ProjectPipelineTestReportManager` + :attr:`gitlab.v4.objects.ProjectPipeline.test_report` * GitLab API: https://docs.gitlab.com/ee/api/pipelines.html#get-a-pipelines-test-report Examples -------- Get the test report for a pipeline:: test_report = pipeline.test_report.get() python-gitlab-2.10.1/docs/gl_objects/project_access_tokens.rst000066400000000000000000000013771416141341200245300ustar00rootroot00000000000000##################### Project Access Tokens ##################### Get a list of project access tokens References ---------- * v4 API: + :class:`gitlab.v4.objects.ProjectAccessToken` + :class:`gitlab.v4.objects.ProjectAccessTokenManager` + :attr:`gitlab.Gitlab.project_access_tokens` * GitLab API: https://docs.gitlab.com/ee/api/resource_access_tokens.html Examples -------- List project access tokens:: access_tokens = gl.projects.get(1, lazy=True).access_tokens.list() print(access_tokens[0].name) Create project access token:: access_token = gl.projects.get(1).access_tokens.create({"name": "test", "scopes": ["api"]}) Revoke a project access tokens:: gl.projects.get(1).access_tokens.delete(42) # or access_token.delete() python-gitlab-2.10.1/docs/gl_objects/projects.rst000066400000000000000000000435001416141341200220010ustar00rootroot00000000000000######## 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) # Get a project by name with namespace project_name_with_namespace = "namespace/project_name" project = gl.projects.get(project_name_with_namespace) 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') # or project.files.delete(file_path='testfile.txt', 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'}) 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` + :class:`gitlab.v4.objects.ProjectMemberAllManager` + :attr:`gitlab.v4.objects.Project.members` + :attr:`gitlab.v4.objects.Project.members_all` * GitLab API: https://docs.gitlab.com/ce/api/members.html Examples -------- List only direct project members:: members = project.members.list() List the project members recursively (including inherited members through ancestor groups):: members = project.members_all.list(all=True) # or members = project.members.all(all=True) # Deprecated Search project members matching a query string:: members = project.members.list(query='bar') Get only direct project member:: member = project.members.get(user_id) Get a member of a project, including members inherited through ancestor groups:: members = project.members_all.get(member_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 active project services:: service = project.services.list() 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 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'] python-gitlab-2.10.1/docs/gl_objects/protected_branches.rst000066400000000000000000000024721416141341200240110ustar00rootroot00000000000000################## 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.10.1/docs/gl_objects/releases.rst000066400000000000000000000031671416141341200217600ustar00rootroot00000000000000######## Releases ######## 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') Edit a release:: release.name = "Demo Release" release.description = "release notes go here" release.save() 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:: # via its tag name from project attributes release = project.releases.delete('v1.2.3') # delete object directly release.delete() Project release links ===================== Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectReleaseLink` + :class:`gitlab.v4.objects.ProjectReleaseLinkManager` + :attr:`gitlab.v4.objects.ProjectRelease.links` * Gitlab API: https://docs.gitlab.com/ee/api/releases/links.html Examples -------- Get a list of releases from a project:: links = release.links.list() Get a single release link:: link = release.links.get(1) Create a release link for a release:: link = release.links.create({"url": "https://example.com/asset", "name": "asset"}) Delete a release link:: # via its ID from release attributes release.links.delete(1) # delete object directly link.delete() python-gitlab-2.10.1/docs/gl_objects/remote_mirrors.rst000066400000000000000000000015161416141341200232210ustar00rootroot00000000000000###################### Project Remote Mirrors ###################### Remote Mirrors allow you to set up push mirroring for a project. References ========== * v4 API: + :class:`gitlab.v4.objects.ProjectRemoteMirror` + :class:`gitlab.v4.objects.ProjectRemoteMirrorManager` + :attr:`gitlab.v4.objects.Project.remote_mirrors` * GitLab API: https://docs.gitlab.com/ce/api/remote_mirrors.html Examples -------- Get the list of a project's remote mirrors:: mirrors = project.remote_mirrors.list() Create (and enable) a remote mirror for a project:: mirror = project.remote_mirrors.create({'url': 'https://gitlab.com/example.git', 'enabled': True}) Update an existing remote mirror's attributes:: mirror.enabled = False mirror.only_protected_branches = True mirror.save() python-gitlab-2.10.1/docs/gl_objects/repositories.rst000066400000000000000000000011651416141341200227000ustar00rootroot00000000000000##################### 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.10.1/docs/gl_objects/repository_tags.rst000066400000000000000000000021501416141341200234010ustar00rootroot00000000000000######################## 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.10.1/docs/gl_objects/runners.rst000066400000000000000000000054461416141341200216530ustar00rootroot00000000000000####### 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/Group runners ===================== Reference --------- * v4 API: + :class:`gitlab.v4.objects.ProjectRunner` + :class:`gitlab.v4.objects.ProjectRunnerManager` + :attr:`gitlab.v4.objects.Project.runners` + :class:`gitlab.v4.objects.GroupRunner` + :class:`gitlab.v4.objects.GroupRunnerManager` + :attr:`gitlab.v4.objects.Group.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.10.1/docs/gl_objects/search.rst000066400000000000000000000043371416141341200214220ustar00rootroot00000000000000########## 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. The following constants are provided to represent the possible scopes: * Shared scopes (global, group and project): + ``gitlab.SEARCH_SCOPE_PROJECTS``: ``projects`` + ``gitlab.SEARCH_SCOPE_ISSUES``: ``issues`` + ``gitlab.SEARCH_SCOPE_MERGE_REQUESTS``: ``merge_requests`` + ``gitlab.SEARCH_SCOPE_MILESTONES``: ``milestones`` + ``gitlab.SEARCH_SCOPE_WIKI_BLOBS``: ``wiki_blobs`` + ``gitlab.SEARCH_SCOPE_COMMITS``: ``commits`` + ``gitlab.SEARCH_SCOPE_BLOBS``: ``blobs`` + ``gitlab.SEARCH_SCOPE_USERS``: ``users`` * specific global scope: + ``gitlab.SEARCH_SCOPE_GLOBAL_SNIPPET_TITLES``: ``snippet_titles`` * specific project scope: + ``gitlab.SEARCH_SCOPE_PROJECT_NOTES``: ``notes`` 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(gitlab.SEARCH_SCOPE_ISSUES, 'regression') # group search group = gl.groups.get('mygroup') group.search(gitlab.SEARCH_SCOPE_ISSUES, 'regression') # project search project = gl.projects.get('myproject') project.search(gitlab.SEARCH_SCOPE_ISSUES, 'regression') The ``search()`` methods implement the pagination support:: # get lists of 10 items, and start at page 2 gl.search(gitlab.SEARCH_SCOPE_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(gitlab.SEARCH_SCOPE_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(gitlab.SEARCH_SCOPE_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.10.1/docs/gl_objects/settings.rst000066400000000000000000000006411416141341200220070ustar00rootroot00000000000000######## 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.10.1/docs/gl_objects/sidekiq.rst000066400000000000000000000006111416141341200215750ustar00rootroot00000000000000############### 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.10.1/docs/gl_objects/snippets.rst000066400000000000000000000026751416141341200220250ustar00rootroot00000000000000######## 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.10.1/docs/gl_objects/system_hooks.rst000066400000000000000000000011241416141341200226730ustar00rootroot00000000000000############ 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.10.1/docs/gl_objects/templates.rst000066400000000000000000000040161416141341200221450ustar00rootroot00000000000000######### 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.10.1/docs/gl_objects/todos.rst000066400000000000000000000015101416141341200212730ustar00rootroot00000000000000##### 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.10.1/docs/gl_objects/users.rst000066400000000000000000000202101416141341200213020ustar00rootroot00000000000000###################### 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() Follow/Unfollow a user:: user.follow() user.unfollow() 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() Delete an external identity by provider name:: user.identityproviders.delete('oauth2_generic') Get the followers of a user user.followers_users.list() Get the followings of a user user.following_users.list() 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() User memberships ========================= References ---------- * v4 API: + :class:`gitlab.v4.objects.UserMembership` + :class:`gitlab.v4.objects.UserMembershipManager` + :attr:`gitlab.v4.objects.User.memberships` * GitLab API: https://docs.gitlab.com/ee/api/users.html#user-memberships-admin-only List direct memberships for a user:: memberships = user.memberships.list() List only direct project memberships:: memberships = user.memberships.list(type='Project') List only direct group memberships:: memberships = user.memberships.list(type='Namespace') 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.10.1/docs/gl_objects/variables.rst000066400000000000000000000041531416141341200221210ustar00rootroot00000000000000############### CI/CD Variables ############### You can configure variables at the instance-level (admin only), or associate variables to projects and groups, to modify pipeline/job scripts behavior. Instance-level variables ======================== This endpoint requires admin access. Reference --------- * v4 API + :class:`gitlab.v4.objects.Variable` + :class:`gitlab.v4.objects.VariableManager` + :attr:`gitlab.Gitlab.variables` * GitLab API + https://docs.gitlab.com/ce/api/instance_level_ci_variables.html Examples -------- List all instance variables:: variables = gl.variables.list() Get an instance variable by key:: variable = gl.variables.get('key_name') Create an instance variable:: variable = gl.variables.create({'key': 'key1', 'value': 'value1'}) Update a variable value:: variable.value = 'new_value' variable.save() Remove a variable:: gl.variables.delete('key_name') # or variable.delete() Projects and groups variables ============================= 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/instance_level_ci_variables.html + 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() python-gitlab-2.10.1/docs/gl_objects/wikis.rst000066400000000000000000000026361416141341200213030ustar00rootroot00000000000000########## Wiki pages ########## References ========== * v4 API: + :class:`gitlab.v4.objects.ProjectWiki` + :class:`gitlab.v4.objects.ProjectWikiManager` + :attr:`gitlab.v4.objects.Project.wikis` + :class:`gitlab.v4.objects.GroupWiki` + :class:`gitlab.v4.objects.GroupWikiManager` + :attr:`gitlab.v4.objects.Group.wikis` * GitLab API for Projects: https://docs.gitlab.com/ce/api/wikis.html * GitLab API for Groups: https://docs.gitlab.com/ee/api/group_wikis.html Examples -------- Get the list of wiki pages for a project. These do not contain the contents of the wiki page. You will need to call get(slug) to retrieve the content by accessing the content attribute:: pages = project.wikis.list() Get the list of wiki pages for a group. These do not contain the contents of the wiki page. You will need to call get(slug) to retrieve the content by accessing the content attribute:: pages = group.wikis.list() Get a single wiki page for a project:: page = project.wikis.get(page_slug) Get a single wiki page for a group:: page = group.wikis.get(page_slug) Get the contents of a wiki page:: print(page.content) Create a wiki page on a project level:: 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.10.1/docs/index.rst000066400000000000000000000011041416141341200171360ustar00rootroot00000000000000.. 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-usage api-usage faq api-objects api/gitlab cli-objects release_notes changelog switching-to-v4 Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` python-gitlab-2.10.1/docs/install.rst000066400000000000000000000010601416141341200174760ustar00rootroot00000000000000############ Installation ############ ``python-gitlab`` is compatible with Python 3.6+. 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.10.1/docs/make.bat000066400000000000000000000150731416141341200167140ustar00rootroot00000000000000@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.10.1/docs/release_notes.rst000066400000000000000000000000421416141341200206570ustar00rootroot00000000000000.. include:: ../RELEASE_NOTES.rst python-gitlab-2.10.1/docs/switching-to-v4.rst000066400000000000000000000062731416141341200210110ustar00rootroot00000000000000.. _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.10.1/gitlab/000077500000000000000000000000001416141341200156135ustar00rootroot00000000000000python-gitlab-2.10.1/gitlab/__init__.py000066400000000000000000000023071416141341200177260ustar00rootroot00000000000000# -*- 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.""" import warnings import gitlab.config # noqa: F401 from gitlab.__version__ import ( # noqa: F401 __author__, __copyright__, __email__, __license__, __title__, __version__, ) from gitlab.client import Gitlab, GitlabList # noqa: F401 from gitlab.const import * # noqa: F401,F403 from gitlab.exceptions import * # noqa: F401,F403 warnings.filterwarnings("default", category=DeprecationWarning, module="^gitlab") python-gitlab-2.10.1/gitlab/__main__.py000066400000000000000000000001041416141341200177000ustar00rootroot00000000000000import gitlab.cli if __name__ == "__main__": gitlab.cli.main() python-gitlab-2.10.1/gitlab/__version__.py000066400000000000000000000003721416141341200204500ustar00rootroot00000000000000__author__ = "Gauvain Pocentek, python-gitlab team" __copyright__ = "Copyright 2013-2019 Gauvain Pocentek, 2019-2021 python-gitlab team" __email__ = "gauvainpocentek@gmail.com" __license__ = "LGPL3" __title__ = "python-gitlab" __version__ = "2.10.1" python-gitlab-2.10.1/gitlab/base.py000066400000000000000000000246241416141341200171070ustar00rootroot00000000000000# -*- 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 from types import ModuleType from typing import Any, Dict, Iterable, NamedTuple, Optional, Tuple, Type from gitlab import types as g_types from gitlab.exceptions import GitlabParsingError from .client import Gitlab, GitlabList __all__ = [ "RequiredOptional", "RESTObject", "RESTObjectList", "RESTManager", ] 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: Optional[str] = "id" _attrs: Dict[str, Any] _module: ModuleType _parent_attrs: Dict[str, Any] _short_print_attr: Optional[str] = None _updated_attrs: Dict[str, Any] manager: "RESTManager" def __init__(self, manager: "RESTManager", attrs: Dict[str, Any]) -> None: if not isinstance(attrs, dict): raise GitlabParsingError( "Attempted to initialize RESTObject with a non-dictionary value: " "{!r}\nThis likely indicates an incorrect or malformed server " "response.".format(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) -> Dict[str, Any]: state = self.__dict__.copy() module = state.pop("_module") state["_module_name"] = module.__name__ return state def __setstate__(self, state: Dict[str, Any]) -> None: module_name = state.pop("_module_name") self.__dict__.update(state) self.__dict__["_module"] = importlib.import_module(module_name) def __getattr__(self, name: str) -> Any: 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: str, value: Any) -> None: self.__dict__["_updated_attrs"][name] = value def __str__(self) -> str: data = self._attrs.copy() data.update(self._updated_attrs) return "%s => %s" % (type(self), data) def __repr__(self) -> str: 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: object) -> bool: if not isinstance(other, RESTObject): return NotImplemented if self.get_id() and other.get_id(): return self.get_id() == other.get_id() return super(RESTObject, self) == other def __ne__(self, other: object) -> bool: if not isinstance(other, RESTObject): return NotImplemented if self.get_id() and other.get_id(): return self.get_id() != other.get_id() return super(RESTObject, self) != other def __dir__(self) -> Iterable[str]: return set(self.attributes).union(super(RESTObject, self).__dir__()) def __hash__(self) -> int: if not self.get_id(): return super(RESTObject, self).__hash__() return hash(self.get_id()) def _create_managers(self) -> None: 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: Dict[str, Any]) -> None: self.__dict__["_updated_attrs"] = {} self.__dict__["_attrs"] = new_attrs def get_id(self) -> Any: """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) -> Dict[str, Any]: 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: "RESTManager", obj_cls: Type[RESTObject], _list: GitlabList ) -> None: """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) -> "RESTObjectList": return self def __len__(self) -> int: return len(self._list) def __next__(self) -> RESTObject: return self.next() def next(self) -> RESTObject: data = self._list.next() return self._obj_cls(self.manager, data) @property def current_page(self) -> int: """The current page number.""" return self._list.current_page @property def prev_page(self) -> Optional[int]: """The previous page number. If None, the current page is the first. """ return self._list.prev_page @property def next_page(self) -> Optional[int]: """The next page number. If None, the current page is the last. """ return self._list.next_page @property def per_page(self) -> int: """The number of items per page.""" return self._list.per_page @property def total_pages(self) -> int: """The total number of pages.""" return self._list.total_pages @property def total(self) -> int: """The total number of items.""" return self._list.total class RequiredOptional(NamedTuple): required: Tuple[str, ...] = tuple() optional: Tuple[str, ...] = tuple() 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 """ _create_attrs: RequiredOptional = RequiredOptional() _update_attrs: RequiredOptional = RequiredOptional() _path: Optional[str] = None _obj_cls: Optional[Type[RESTObject]] = None _from_parent_attrs: Dict[str, Any] = {} _types: Dict[str, Type[g_types.GitlabAttribute]] = {} _computed_path: Optional[str] _parent: Optional[RESTObject] _parent_attrs: Dict[str, Any] gitlab: Gitlab def __init__(self, gl: Gitlab, parent: Optional[RESTObject] = None) -> 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) -> Optional[Dict[str, Any]]: return self._parent_attrs def _compute_path(self, path: Optional[str] = None) -> Optional[str]: self._parent_attrs = {} if path is None: path = self._path if path is None: return None if self._parent is None or not 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) -> Optional[str]: return self._computed_path python-gitlab-2.10.1/gitlab/cli.py000066400000000000000000000173731416141341200167470ustar00rootroot00000000000000#!/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 . import argparse import functools import re import sys from types import ModuleType from typing import Any, Callable, cast, Dict, Optional, Tuple, Type, TypeVar, Union from requests.structures import CaseInsensitiveDict import gitlab.config from gitlab.base import RESTObject # This regex is based on: # https://github.com/jpvanhal/inflection/blob/master/inflection/__init__.py camel_upperlower_regex = re.compile(r"([A-Z]+)([A-Z][a-z])") camel_lowerupper_regex = re.compile(r"([a-z\d])([A-Z])") # custom_actions = { # cls: { # action: (mandatory_args, optional_args, in_obj), # }, # } custom_actions: Dict[str, Dict[str, Tuple[Tuple[str, ...], Tuple[str, ...], bool]]] = {} # For an explanation of how these type-hints work see: # https://mypy.readthedocs.io/en/stable/generics.html#declaring-decorators # # The goal here is that functions which get decorated will retain their types. __F = TypeVar("__F", bound=Callable[..., Any]) def register_custom_action( cls_names: Union[str, Tuple[str, ...]], mandatory: Tuple[str, ...] = tuple(), optional: Tuple[str, ...] = tuple(), custom_action: Optional[str] = None, ) -> Callable[[__F], __F]: def wrap(f: __F) -> __F: @functools.wraps(f) def wrapped_f(*args: Any, **kwargs: Any) -> Any: return f(*args, **kwargs) # in_obj defines whether the method belongs to the obj or the manager in_obj = True if isinstance(cls_names, tuple): classes = cls_names else: 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 = custom_action or f.__name__.replace("_", "-") custom_actions[final_name][action] = (mandatory, optional, in_obj) return cast(__F, wrapped_f) return wrap def die(msg: str, e: Optional[Exception] = None) -> None: if e: msg = "%s (%s)" % (msg, e) sys.stderr.write(msg + "\n") sys.exit(1) def what_to_cls(what: str, namespace: ModuleType) -> Type[RESTObject]: classes = CaseInsensitiveDict(namespace.__dict__) lowercase_class = what.replace("-", "") return classes[lowercase_class] def cls_to_what(cls: RESTObject) -> str: dasherized_uppercase = camel_upperlower_regex.sub(r"\1-\2", cls.__name__) dasherized_lowercase = camel_lowerupper_regex.sub(r"\1-\2", dasherized_uppercase) return dasherized_lowercase.lower() def _get_base_parser(add_help: bool = True) -> argparse.ArgumentParser: 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() -> argparse.ArgumentParser: # NOTE: We must delay import of gitlab.v4.cli until now or # otherwise it will cause circular import errors import gitlab.v4.cli parser = _get_base_parser() return gitlab.v4.cli.extend_parser(parser) def _parse_value(v: Any) -> Any: 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 docs() -> argparse.ArgumentParser: """ Provide a statically generated parser for sphinx only, so we don't need to provide dummy gitlab config for readthedocs. """ if "sphinx" not in sys.modules: sys.exit("Docs parser is only intended for build_sphinx") return _get_parser() def main() -> None: 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, _) = 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) # We only support v4 API at this time if config.api_version not in ("4",): raise ModuleNotFoundError(name="gitlab.v%s.cli" % config.api_version) # Now we build the entire set of subcommands and do the complete parsing parser = _get_parser() try: import argcomplete # type: ignore argcomplete.autocomplete(parser) except Exception: pass args = parser.parse_args() 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_dict = vars(args) # Remove CLI behavior-related args for item in ( "gitlab", "config_file", "verbose", "debug", "what", "whaction", "version", "output", ): args_dict.pop(item) args_dict = {k: _parse_value(v) for k, v in args_dict.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() gitlab.v4.cli.run(gl, what, action, args_dict, verbose, output, fields) python-gitlab-2.10.1/gitlab/client.py000066400000000000000000001115651416141341200174540ustar00rootroot00000000000000# -*- 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.""" import time from typing import Any, cast, Dict, List, Optional, Tuple, TYPE_CHECKING, Union import requests import requests.utils from requests_toolbelt.multipart.encoder import MultipartEncoder # type: ignore import gitlab.config import gitlab.const import gitlab.exceptions from gitlab import utils REDIRECT_MSG = ( "python-gitlab detected an http to https redirection. You " "must update your GitLab URL to use https:// to avoid issues." ) 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 user_agent (str): A custom user agent to use for making HTTP requests. """ def __init__( self, url: str, private_token: Optional[str] = None, oauth_token: Optional[str] = None, job_token: Optional[str] = None, ssl_verify: Union[bool, str] = True, http_username: Optional[str] = None, http_password: Optional[str] = None, timeout: Optional[float] = None, api_version: str = "4", session: Optional[requests.Session] = None, per_page: Optional[int] = None, pagination: Optional[str] = None, order_by: Optional[str] = None, user_agent: str = gitlab.const.USER_AGENT, ) -> None: self._api_version = str(api_version) self._server_version: Optional[str] = None self._server_revision: Optional[str] = None self._base_url = url.rstrip("/") self._url = "%s/api/v%s" % (self._base_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": user_agent} #: 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 # We only support v4 API at this time if self._api_version not in ("4",): raise ModuleNotFoundError(name="gitlab.v%s.objects" % self._api_version) # NOTE: We must delay import of gitlab.v4.objects until now or # otherwise it will cause circular import errors import gitlab.v4.objects objects = gitlab.v4.objects self._objects = objects self.broadcastmessages = objects.BroadcastMessageManager(self) """See :class:`~gitlab.v4.objects.BroadcastMessageManager`""" self.deploykeys = objects.DeployKeyManager(self) """See :class:`~gitlab.v4.objects.DeployKeyManager`""" self.deploytokens = objects.DeployTokenManager(self) """See :class:`~gitlab.v4.objects.DeployTokenManager`""" self.geonodes = objects.GeoNodeManager(self) """See :class:`~gitlab.v4.objects.GeoNodeManager`""" self.gitlabciymls = objects.GitlabciymlManager(self) """See :class:`~gitlab.v4.objects.GitlabciymlManager`""" self.gitignores = objects.GitignoreManager(self) """See :class:`~gitlab.v4.objects.GitignoreManager`""" self.groups = objects.GroupManager(self) """See :class:`~gitlab.v4.objects.GroupManager`""" self.hooks = objects.HookManager(self) """See :class:`~gitlab.v4.objects.HookManager`""" self.issues = objects.IssueManager(self) """See :class:`~gitlab.v4.objects.IssueManager`""" self.issues_statistics = objects.IssuesStatisticsManager(self) """See :class:`~gitlab.v4.objects.IssuesStatisticsManager`""" self.keys = objects.KeyManager(self) """See :class:`~gitlab.v4.objects.KeyManager`""" self.ldapgroups = objects.LDAPGroupManager(self) """See :class:`~gitlab.v4.objects.LDAPGroupManager`""" self.licenses = objects.LicenseManager(self) """See :class:`~gitlab.v4.objects.LicenseManager`""" self.namespaces = objects.NamespaceManager(self) """See :class:`~gitlab.v4.objects.NamespaceManager`""" self.mergerequests = objects.MergeRequestManager(self) """See :class:`~gitlab.v4.objects.MergeRequestManager`""" self.notificationsettings = objects.NotificationSettingsManager(self) """See :class:`~gitlab.v4.objects.NotificationSettingsManager`""" self.projects = objects.ProjectManager(self) """See :class:`~gitlab.v4.objects.ProjectManager`""" self.runners = objects.RunnerManager(self) """See :class:`~gitlab.v4.objects.RunnerManager`""" self.settings = objects.ApplicationSettingsManager(self) """See :class:`~gitlab.v4.objects.ApplicationSettingsManager`""" self.appearance = objects.ApplicationAppearanceManager(self) """See :class:`~gitlab.v4.objects.ApplicationAppearanceManager`""" self.sidekiq = objects.SidekiqManager(self) """See :class:`~gitlab.v4.objects.SidekiqManager`""" self.snippets = objects.SnippetManager(self) """See :class:`~gitlab.v4.objects.SnippetManager`""" self.users = objects.UserManager(self) """See :class:`~gitlab.v4.objects.UserManager`""" self.todos = objects.TodoManager(self) """See :class:`~gitlab.v4.objects.TodoManager`""" self.dockerfiles = objects.DockerfileManager(self) """See :class:`~gitlab.v4.objects.DockerfileManager`""" self.events = objects.EventManager(self) """See :class:`~gitlab.v4.objects.EventManager`""" self.audit_events = objects.AuditEventManager(self) """See :class:`~gitlab.v4.objects.AuditEventManager`""" self.features = objects.FeatureManager(self) """See :class:`~gitlab.v4.objects.FeatureManager`""" self.pagesdomains = objects.PagesDomainManager(self) """See :class:`~gitlab.v4.objects.PagesDomainManager`""" self.user_activities = objects.UserActivitiesManager(self) """See :class:`~gitlab.v4.objects.UserActivitiesManager`""" self.applications = objects.ApplicationManager(self) """See :class:`~gitlab.v4.objects.ApplicationManager`""" self.variables = objects.VariableManager(self) """See :class:`~gitlab.v4.objects.VariableManager`""" self.personal_access_tokens = objects.PersonalAccessTokenManager(self) """See :class:`~gitlab.v4.objects.PersonalAccessTokenManager`""" def __enter__(self) -> "Gitlab": return self def __exit__(self, *args: Any) -> None: self.session.close() def __getstate__(self) -> Dict[str, Any]: state = self.__dict__.copy() state.pop("_objects") return state def __setstate__(self, state: Dict[str, Any]) -> None: self.__dict__.update(state) # We only support v4 API at this time if self._api_version not in ("4",): raise ModuleNotFoundError(name="gitlab.v%s.objects" % self._api_version) # NOTE: We must delay import of gitlab.v4.objects until now or # otherwise it will cause circular import errors import gitlab.v4.objects self._objects = gitlab.v4.objects @property def url(self) -> str: """The user-provided server URL.""" return self._base_url @property def api_url(self) -> str: """The computed API base URL.""" return self._url @property def api_version(self) -> str: """The API version used (4 only).""" return self._api_version @classmethod def from_config( cls, gitlab_id: Optional[str] = None, config_files: Optional[List[str]] = None ) -> "Gitlab": """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, user_agent=config.user_agent, ) def auth(self) -> None: """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) -> Tuple[str, str]: """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") if isinstance(data, dict): self._server_version = data["version"] self._server_revision = data["revision"] else: self._server_version = "unknown" self._server_revision = "unknown" except Exception: self._server_version = "unknown" self._server_revision = "unknown" return cast(str, self._server_version), cast(str, self._server_revision) @gitlab.exceptions.on_http_error(gitlab.exceptions.GitlabVerifyError) def lint(self, content: str, **kwargs: Any) -> Tuple[bool, List[str]]: """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) if TYPE_CHECKING: assert not isinstance(data, requests.Response) return (data["status"] == "valid", data["errors"]) @gitlab.exceptions.on_http_error(gitlab.exceptions.GitlabMarkdownError) def markdown( self, text: str, gfm: bool = False, project: Optional[str] = None, **kwargs: Any ) -> str: """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) if TYPE_CHECKING: assert not isinstance(data, requests.Response) return data["html"] @gitlab.exceptions.on_http_error(gitlab.exceptions.GitlabLicenseError) def get_license(self, **kwargs: Any) -> Dict[str, Any]: """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 """ result = self.http_get("/license", **kwargs) if isinstance(result, dict): return result return {} @gitlab.exceptions.on_http_error(gitlab.exceptions.GitlabLicenseError) def set_license(self, license: str, **kwargs: Any) -> Dict[str, Any]: """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} result = self.http_post("/license", post_data=data, **kwargs) if TYPE_CHECKING: assert not isinstance(result, requests.Response) return result def _set_auth_info(self) -> None: 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) -> None: import logging from http.client import HTTPConnection # noqa HTTPConnection.debuglevel = 1 # type: ignore logging.basicConfig() logging.getLogger().setLevel(logging.DEBUG) requests_log = logging.getLogger("requests.packages.urllib3") requests_log.setLevel(logging.DEBUG) requests_log.propagate = True def _get_session_opts(self) -> Dict[str, Any]: return { "headers": self.headers.copy(), "auth": self._http_auth, "timeout": self.timeout, "verify": self.ssl_verify, } def _build_url(self, path: str) -> str: """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: requests.Response) -> None: # 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 gitlab.exceptions.RedirectError(REDIRECT_MSG) def _prepare_send_data( self, files: Optional[Dict[str, Any]] = None, post_data: Optional[Dict[str, Any]] = None, raw: bool = False, ) -> Tuple[ Optional[Dict[str, Any]], Optional[Union[Dict[str, Any], MultipartEncoder]], str, ]: if files: if post_data is None: post_data = {} else: # booleans does not exists for data (neither for MultipartEncoder): # cast to string int to avoid: 'bool' object has no attribute 'encode' for k, v in post_data.items(): if isinstance(v, bool): post_data[k] = str(int(v)) post_data["file"] = files.get("file") post_data["avatar"] = files.get("avatar") data = MultipartEncoder(post_data) return (None, data, data.content_type) if raw and post_data: return (None, post_data, "application/octet-stream") return (post_data, None, "application/json") def http_request( self, verb: str, path: str, query_data: Optional[Dict[str, Any]] = None, post_data: Optional[Dict[str, Any]] = None, raw: bool = False, streamed: bool = False, files: Optional[Dict[str, Any]] = None, timeout: Optional[float] = None, obey_rate_limit: bool = True, retry_transient_errors: bool = False, max_retries: int = 10, **kwargs: Any, ) -> requests.Response: """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 by default) raw (bool): If True, do not convert post_data to json streamed (bool): Whether the data should be streamed files (dict): The files to send to the server timeout (float): The timeout, in seconds, for the request obey_rate_limit (bool): Whether to obey 429 Too Many Request responses. Defaults to True. retry_transient_errors (bool): Whether to retry after 500, 502, 503, or 504 responses. Defaults to False. max_retries (int): Max retries after 429 or transient errors, set to -1 to retry forever. Defaults to 10. **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: Dict[str, Any] = {} 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() verify = opts.pop("verify") opts_timeout = opts.pop("timeout") # If timeout was passed into kwargs, allow it to override the default if timeout is None: timeout = opts_timeout # We need to deal with json vs. data when uploading files json, data, content_type = self._prepare_send_data(files, post_data, raw) opts["headers"]["Content-type"] = content_type # 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, **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 ) 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 gitlab.exceptions.GitlabAuthenticationError( response_code=result.status_code, error_message=error_message, response_body=result.content, ) raise gitlab.exceptions.GitlabHttpError( response_code=result.status_code, error_message=error_message, response_body=result.content, ) def http_get( self, path: str, query_data: Optional[Dict[str, Any]] = None, streamed: bool = False, raw: bool = False, **kwargs: Any, ) -> Union[Dict[str, Any], requests.Response]: """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 as e: raise gitlab.exceptions.GitlabParsingError( error_message="Failed to parse the server message" ) from e else: return result def http_list( self, path: str, query_data: Optional[Dict[str, Any]] = None, as_list: Optional[bool] = None, **kwargs: Any, ) -> Union["GitlabList", List[Dict[str, Any]]]: """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/projects') 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) page = kwargs.get("page") if get_all is True and as_list is True: return list(GitlabList(self, url, query_data, **kwargs)) if page 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: str, query_data: Optional[Dict[str, Any]] = None, post_data: Optional[Dict[str, Any]] = None, raw: bool = False, files: Optional[Dict[str, Any]] = None, **kwargs: Any, ) -> Union[Dict[str, Any], requests.Response]: """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 by default) raw (bool): If True, do not convert post_data 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 as e: raise gitlab.exceptions.GitlabParsingError( error_message="Failed to parse the server message" ) from e return result def http_put( self, path: str, query_data: Optional[Dict[str, Any]] = None, post_data: Optional[Dict[str, Any]] = None, raw: bool = False, files: Optional[Dict[str, Any]] = None, **kwargs: Any, ) -> Union[Dict[str, Any], requests.Response]: """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 by default) raw (bool): If True, do not convert post_data 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, raw=raw, **kwargs, ) try: return result.json() except Exception as e: raise gitlab.exceptions.GitlabParsingError( error_message="Failed to parse the server message" ) from e def http_delete(self, path: str, **kwargs: Any) -> requests.Response: """Make a DELETE 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) @gitlab.exceptions.on_http_error(gitlab.exceptions.GitlabSearchError) def search( self, scope: str, search: str, **kwargs: Any ) -> Union["GitlabList", List[Dict[str, Any]]]: """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: Gitlab, url: str, query_data: Dict[str, Any], get_next: bool = True, **kwargs: Any, ) -> None: self._gl = gl # Preserve kwargs for subsequent queries self._kwargs = kwargs.copy() self._query(url, query_data, **self._kwargs) self._get_next = get_next # Remove query_parameters from kwargs, which are saved via the `next` URL self._kwargs.pop("query_parameters", None) def _query( self, url: str, query_data: Optional[Dict[str, Any]] = None, **kwargs: Any ) -> None: query_data = query_data or {} result = self._gl.http_request("get", url, query_data=query_data, **kwargs) try: links = result.links if links: next_url = links["next"]["url"] else: next_url = requests.utils.parse_header_links(result.headers["links"])[ 0 ]["url"] self._next_url = next_url except KeyError: self._next_url = None self._current_page: Optional[Union[str, int]] = result.headers.get("X-Page") self._prev_page: Optional[Union[str, int]] = result.headers.get("X-Prev-Page") self._next_page: Optional[Union[str, int]] = result.headers.get("X-Next-Page") self._per_page: Optional[Union[str, int]] = result.headers.get("X-Per-Page") self._total_pages: Optional[Union[str, int]] = result.headers.get( "X-Total-Pages" ) self._total: Optional[Union[str, int]] = result.headers.get("X-Total") try: self._data: List[Dict[str, Any]] = result.json() except Exception as e: raise gitlab.exceptions.GitlabParsingError( error_message="Failed to parse the server message" ) from e self._current = 0 @property def current_page(self) -> int: """The current page number.""" if TYPE_CHECKING: assert self._current_page is not None return int(self._current_page) @property def prev_page(self) -> Optional[int]: """The previous page number. If None, the current page is the first. """ return int(self._prev_page) if self._prev_page else None @property def next_page(self) -> Optional[int]: """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) -> int: """The number of items per page.""" if TYPE_CHECKING: assert self._per_page is not None return int(self._per_page) @property def total_pages(self) -> int: """The total number of pages.""" if TYPE_CHECKING: assert self._total_pages is not None return int(self._total_pages) @property def total(self) -> int: """The total number of items.""" if TYPE_CHECKING: assert self._total is not None return int(self._total) def __iter__(self) -> "GitlabList": return self def __len__(self) -> int: if self._total is None: return 0 return int(self._total) def __next__(self) -> Dict[str, Any]: return self.next() def next(self) -> Dict[str, Any]: 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, **self._kwargs) return self.next() raise StopIteration python-gitlab-2.10.1/gitlab/config.py000066400000000000000000000166051416141341200174420ustar00rootroot00000000000000# -*- 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 configparser import os import shlex import subprocess from os.path import expanduser, expandvars from typing import List, Optional, Union from gitlab.const import USER_AGENT def _env_config() -> List[str]: if "PYTHON_GITLAB_CFG" in os.environ: return [os.environ["PYTHON_GITLAB_CFG"]] return [] _DEFAULT_FILES: List[str] = _env_config() + [ "/etc/python-gitlab.cfg", os.path.expanduser("~/.python-gitlab.cfg"), ] HELPER_PREFIX = "helper:" HELPER_ATTRIBUTES = ["job_token", "http_password", "private_token", "oauth_token"] class ConfigError(Exception): pass class GitlabIDError(ConfigError): pass class GitlabDataError(ConfigError): pass class GitlabConfigMissingError(ConfigError): pass class GitlabConfigHelperError(ConfigError): pass class GitlabConfigParser(object): def __init__( self, gitlab_id: Optional[str] = None, config_files: Optional[List[str]] = None ) -> 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 as e: raise GitlabIDError( "Impossible to get the gitlab id (not specified in config file)" ) from e try: self.url = self._config.get(self.gitlab_id, "url") except Exception as e: raise GitlabDataError( "Impossible to get gitlab informations from " "configuration (%s)" % self.gitlab_id ) from e self.ssl_verify: Union[bool, str] = 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._get_values_from_helper() 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 self.user_agent = USER_AGENT try: self.user_agent = self._config.get("global", "user_agent") except Exception: pass try: self.user_agent = self._config.get(self.gitlab_id, "user_agent") except Exception: pass def _get_values_from_helper(self) -> None: """Update attributes that may get values from an external helper program""" for attr in HELPER_ATTRIBUTES: value = getattr(self, attr) if not isinstance(value, str): continue if not value.lower().strip().startswith(HELPER_PREFIX): continue helper = value[len(HELPER_PREFIX) :].strip() commmand = [expanduser(expandvars(token)) for token in shlex.split(helper)] try: value = ( subprocess.check_output(commmand, stderr=subprocess.PIPE) .decode("utf-8") .strip() ) except subprocess.CalledProcessError as e: stderr = e.stderr.decode().strip() raise GitlabConfigHelperError( f"Failed to read {attr} value from helper " f"for {self.gitlab_id}:\n{stderr}" ) from e setattr(self, attr, value) python-gitlab-2.10.1/gitlab/const.py000066400000000000000000000036531416141341200173220ustar00rootroot00000000000000# -*- 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 gitlab.__version__ import __title__, __version__ NO_ACCESS: int = 0 MINIMAL_ACCESS: int = 5 GUEST_ACCESS: int = 10 REPORTER_ACCESS: int = 20 DEVELOPER_ACCESS: int = 30 MAINTAINER_ACCESS: int = 40 MASTER_ACCESS: int = MAINTAINER_ACCESS OWNER_ACCESS: int = 50 VISIBILITY_PRIVATE: int = 0 VISIBILITY_INTERNAL: int = 10 VISIBILITY_PUBLIC: int = 20 NOTIFICATION_LEVEL_DISABLED: str = "disabled" NOTIFICATION_LEVEL_PARTICIPATING: str = "participating" NOTIFICATION_LEVEL_WATCH: str = "watch" NOTIFICATION_LEVEL_GLOBAL: str = "global" NOTIFICATION_LEVEL_MENTION: str = "mention" NOTIFICATION_LEVEL_CUSTOM: str = "custom" # Search scopes # all scopes (global, group and project) SEARCH_SCOPE_PROJECTS: str = "projects" SEARCH_SCOPE_ISSUES: str = "issues" SEARCH_SCOPE_MERGE_REQUESTS: str = "merge_requests" SEARCH_SCOPE_MILESTONES: str = "milestones" SEARCH_SCOPE_WIKI_BLOBS: str = "wiki_blobs" SEARCH_SCOPE_COMMITS: str = "commits" SEARCH_SCOPE_BLOBS: str = "blobs" SEARCH_SCOPE_USERS: str = "users" # specific global scope SEARCH_SCOPE_GLOBAL_SNIPPET_TITLES: str = "snippet_titles" # specific project scope SEARCH_SCOPE_PROJECT_NOTES: str = "notes" USER_AGENT: str = "{}/{}".format(__title__, __version__) python-gitlab-2.10.1/gitlab/exceptions.py000066400000000000000000000141701416141341200203510ustar00rootroot00000000000000# -*- 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 from typing import Any, Callable, cast, Optional, Type, TYPE_CHECKING, TypeVar, Union class GitlabError(Exception): def __init__( self, error_message: Union[str, bytes] = "", response_code: Optional[int] = None, response_body: Optional[bytes] = None, ) -> 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) if TYPE_CHECKING: assert isinstance(error_message, bytes) self.error_message = error_message.decode() except Exception: if TYPE_CHECKING: assert isinstance(error_message, str) self.error_message = error_message def __str__(self) -> str: 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 GitlabPipelinePlayError(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 GitlabImportError(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 GitlabRevertError(GitlabOperationError): pass class GitlabLicenseError(GitlabOperationError): pass class GitlabFollowError(GitlabOperationError): pass class GitlabUnfollowError(GitlabOperationError): pass # For an explanation of how these type-hints work see: # https://mypy.readthedocs.io/en/stable/generics.html#declaring-decorators # # The goal here is that functions which get decorated will retain their types. __F = TypeVar("__F", bound=Callable[..., Any]) def on_http_error(error: Type[Exception]) -> Callable[[__F], __F]: """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: __F) -> __F: @functools.wraps(f) def wrapped_f(*args: Any, **kwargs: Any) -> Any: try: return f(*args, **kwargs) except GitlabHttpError as e: raise error(e.error_message, e.response_code, e.response_body) from e return cast(__F, wrapped_f) return wrap python-gitlab-2.10.1/gitlab/mixins.py000066400000000000000000001026761416141341200175100ustar00rootroot00000000000000# -*- 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 warnings from types import ModuleType from typing import ( Any, Callable, Dict, List, Optional, Tuple, Type, TYPE_CHECKING, Union, ) import requests import gitlab from gitlab import base, cli from gitlab import exceptions as exc from gitlab import types as g_types from gitlab import utils __all__ = [ "GetMixin", "GetWithoutIdMixin", "RefreshMixin", "ListMixin", "RetrieveMixin", "CreateMixin", "UpdateMixin", "SetMixin", "DeleteMixin", "CRUDMixin", "NoUpdateMixin", "SaveMixin", "ObjectDeleteMixin", "UserAgentDetailMixin", "AccessRequestMixin", "DownloadMixin", "SubscribableMixin", "TodoMixin", "TimeTrackingMixin", "ParticipantsMixin", "BadgeRenderMixin", ] if TYPE_CHECKING: # When running mypy we use these as the base classes _RestManagerBase = base.RESTManager _RestObjectBase = base.RESTObject else: _RestManagerBase = object _RestObjectBase = object class GetMixin(_RestManagerBase): _computed_path: Optional[str] _from_parent_attrs: Dict[str, Any] _obj_cls: Optional[Type[base.RESTObject]] _optional_get_attrs: Tuple[str, ...] = () _parent: Optional[base.RESTObject] _parent_attrs: Dict[str, Any] _path: Optional[str] gitlab: gitlab.Gitlab @exc.on_http_error(exc.GitlabGetError) def get( self, id: Union[str, int], lazy: bool = False, **kwargs: Any ) -> base.RESTObject: """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 TYPE_CHECKING: assert self._obj_cls is not None if lazy is True: if TYPE_CHECKING: assert self._obj_cls._id_attr is not None return self._obj_cls(self, {self._obj_cls._id_attr: id}) server_data = self.gitlab.http_get(path, **kwargs) if TYPE_CHECKING: assert not isinstance(server_data, requests.Response) return self._obj_cls(self, server_data) class GetWithoutIdMixin(_RestManagerBase): _computed_path: Optional[str] _from_parent_attrs: Dict[str, Any] _obj_cls: Optional[Type[base.RESTObject]] _optional_get_attrs: Tuple[str, ...] = () _parent: Optional[base.RESTObject] _parent_attrs: Dict[str, Any] _path: Optional[str] gitlab: gitlab.Gitlab @exc.on_http_error(exc.GitlabGetError) def get( self, id: Optional[Union[int, str]] = None, **kwargs: Any ) -> Optional[base.RESTObject]: """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 """ if TYPE_CHECKING: assert self.path is not None server_data = self.gitlab.http_get(self.path, **kwargs) if server_data is None: return None if TYPE_CHECKING: assert not isinstance(server_data, requests.Response) assert self._obj_cls is not None return self._obj_cls(self, server_data) class RefreshMixin(_RestObjectBase): _id_attr: Optional[str] _attrs: Dict[str, Any] _module: ModuleType _parent_attrs: Dict[str, Any] _updated_attrs: Dict[str, Any] manager: base.RESTManager @exc.on_http_error(exc.GitlabGetError) def refresh(self, **kwargs: Any) -> None: """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: if TYPE_CHECKING: assert self.manager.path is not None path = self.manager.path server_data = self.manager.gitlab.http_get(path, **kwargs) if TYPE_CHECKING: assert not isinstance(server_data, requests.Response) self._update_attrs(server_data) class ListMixin(_RestManagerBase): _computed_path: Optional[str] _from_parent_attrs: Dict[str, Any] _list_filters: Tuple[str, ...] = () _obj_cls: Optional[Type[base.RESTObject]] _parent: Optional[base.RESTObject] _parent_attrs: Dict[str, Any] _path: Optional[str] gitlab: gitlab.Gitlab @exc.on_http_error(exc.GitlabListError) def list(self, **kwargs: Any) -> Union[base.RESTObjectList, List[base.RESTObject]]: """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 if self._types: for attr_name, type_cls in self._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) if TYPE_CHECKING: assert self._obj_cls is not None 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): _computed_path: Optional[str] _from_parent_attrs: Dict[str, Any] _obj_cls: Optional[Type[base.RESTObject]] _parent: Optional[base.RESTObject] _parent_attrs: Dict[str, Any] _path: Optional[str] gitlab: gitlab.Gitlab pass class CreateMixin(_RestManagerBase): _computed_path: Optional[str] _from_parent_attrs: Dict[str, Any] _obj_cls: Optional[Type[base.RESTObject]] _parent: Optional[base.RESTObject] _parent_attrs: Dict[str, Any] _path: Optional[str] gitlab: gitlab.Gitlab def _check_missing_create_attrs(self, data: Dict[str, Any]) -> None: missing = [] for attr in self._create_attrs.required: if attr not in data: missing.append(attr) continue if missing: raise AttributeError("Missing attributes: %s" % ", ".join(missing)) @exc.on_http_error(exc.GitlabCreateError) def create( self, data: Optional[Dict[str, Any]] = None, **kwargs: Any ) -> base.RESTObject: """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 """ if data is None: data = {} self._check_missing_create_attrs(data) files = {} # We get the attributes that need some special transformation if self._types: # Duplicate data to avoid messing with what the user sent us data = data.copy() for attr_name, type_cls in self._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 isinstance(type_obj, 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) if TYPE_CHECKING: assert not isinstance(server_data, requests.Response) assert self._obj_cls is not None return self._obj_cls(self, server_data) class UpdateMixin(_RestManagerBase): _computed_path: Optional[str] _from_parent_attrs: Dict[str, Any] _obj_cls: Optional[Type[base.RESTObject]] _parent: Optional[base.RESTObject] _parent_attrs: Dict[str, Any] _path: Optional[str] _update_uses_post: bool = False gitlab: gitlab.Gitlab def _check_missing_update_attrs(self, data: Dict[str, Any]) -> None: if TYPE_CHECKING: assert self._obj_cls is not None # Remove the id field from the required list as it was previously moved # to the http path. required = tuple( [k for k in self._update_attrs.required if k != self._obj_cls._id_attr] ) 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_method( self, ) -> Callable[..., Union[Dict[str, Any], requests.Response]]: """Return the HTTP method to use. Returns: object: http_put (default) or http_post """ if self._update_uses_post: 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: Optional[Union[str, int]] = None, new_data: Optional[Dict[str, Any]] = None, **kwargs: Any ) -> Dict[str, Any]: """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 if self._types: # Duplicate data to avoid messing with what the user sent us new_data = new_data.copy() for attr_name, type_cls in self._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 isinstance(type_obj, 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() result = http_method(path, post_data=new_data, files=files, **kwargs) if TYPE_CHECKING: assert not isinstance(result, requests.Response) return result class SetMixin(_RestManagerBase): _computed_path: Optional[str] _from_parent_attrs: Dict[str, Any] _obj_cls: Optional[Type[base.RESTObject]] _parent: Optional[base.RESTObject] _parent_attrs: Dict[str, Any] _path: Optional[str] gitlab: gitlab.Gitlab @exc.on_http_error(exc.GitlabSetError) def set(self, key: str, value: str, **kwargs: Any) -> base.RESTObject: """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) if TYPE_CHECKING: assert not isinstance(server_data, requests.Response) assert self._obj_cls is not None return self._obj_cls(self, server_data) class DeleteMixin(_RestManagerBase): _computed_path: Optional[str] _from_parent_attrs: Dict[str, Any] _obj_cls: Optional[Type[base.RESTObject]] _parent: Optional[base.RESTObject] _parent_attrs: Dict[str, Any] _path: Optional[str] gitlab: gitlab.Gitlab @exc.on_http_error(exc.GitlabDeleteError) def delete(self, id: Union[str, int], **kwargs: Any) -> None: """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): _computed_path: Optional[str] _from_parent_attrs: Dict[str, Any] _obj_cls: Optional[Type[base.RESTObject]] _parent: Optional[base.RESTObject] _parent_attrs: Dict[str, Any] _path: Optional[str] gitlab: gitlab.Gitlab pass class NoUpdateMixin(GetMixin, ListMixin, CreateMixin, DeleteMixin): _computed_path: Optional[str] _from_parent_attrs: Dict[str, Any] _obj_cls: Optional[Type[base.RESTObject]] _parent: Optional[base.RESTObject] _parent_attrs: Dict[str, Any] _path: Optional[str] gitlab: gitlab.Gitlab pass class SaveMixin(_RestObjectBase): """Mixin for RESTObject's that can be updated.""" _id_attr: Optional[str] _attrs: Dict[str, Any] _module: ModuleType _parent_attrs: Dict[str, Any] _updated_attrs: Dict[str, Any] manager: base.RESTManager def _get_updated_data(self) -> Dict[str, Any]: updated_data = {} for attr in self.manager._update_attrs.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: Any) -> None: """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() if TYPE_CHECKING: assert isinstance(self.manager, UpdateMixin) server_data = self.manager.update(obj_id, updated_data, **kwargs) if server_data is not None: self._update_attrs(server_data) class ObjectDeleteMixin(_RestObjectBase): """Mixin for RESTObject's that can be deleted.""" _id_attr: Optional[str] _attrs: Dict[str, Any] _module: ModuleType _parent_attrs: Dict[str, Any] _updated_attrs: Dict[str, Any] manager: base.RESTManager def delete(self, **kwargs: Any) -> None: """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 """ if TYPE_CHECKING: assert isinstance(self.manager, DeleteMixin) self.manager.delete(self.get_id(), **kwargs) class UserAgentDetailMixin(_RestObjectBase): _id_attr: Optional[str] _attrs: Dict[str, Any] _module: ModuleType _parent_attrs: Dict[str, Any] _updated_attrs: Dict[str, Any] manager: base.RESTManager @cli.register_custom_action(("Snippet", "ProjectSnippet", "ProjectIssue")) @exc.on_http_error(exc.GitlabGetError) def user_agent_detail(self, **kwargs: Any) -> Dict[str, Any]: """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()) result = self.manager.gitlab.http_get(path, **kwargs) if TYPE_CHECKING: assert not isinstance(result, requests.Response) return result class AccessRequestMixin(_RestObjectBase): _id_attr: Optional[str] _attrs: Dict[str, Any] _module: ModuleType _parent_attrs: Dict[str, Any] _updated_attrs: Dict[str, Any] manager: base.RESTManager @cli.register_custom_action( ("ProjectAccessRequest", "GroupAccessRequest"), tuple(), ("access_level",) ) @exc.on_http_error(exc.GitlabUpdateError) def approve( self, access_level: int = gitlab.DEVELOPER_ACCESS, **kwargs: Any ) -> None: """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) if TYPE_CHECKING: assert not isinstance(server_data, requests.Response) self._update_attrs(server_data) class DownloadMixin(_RestObjectBase): _id_attr: Optional[str] _attrs: Dict[str, Any] _module: ModuleType _parent_attrs: Dict[str, Any] _updated_attrs: Dict[str, Any] manager: base.RESTManager @cli.register_custom_action(("GroupExport", "ProjectExport")) @exc.on_http_error(exc.GitlabGetError) def download( self, streamed: bool = False, action: Optional[Callable] = None, chunk_size: int = 1024, **kwargs: Any ) -> Optional[bytes]: """Download the archive of a resource 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 = "%s/download" % (self.manager.path) result = self.manager.gitlab.http_get( path, streamed=streamed, raw=True, **kwargs ) if TYPE_CHECKING: assert isinstance(result, requests.Response) return utils.response_content(result, streamed, action, chunk_size) class SubscribableMixin(_RestObjectBase): _id_attr: Optional[str] _attrs: Dict[str, Any] _module: ModuleType _parent_attrs: Dict[str, Any] _updated_attrs: Dict[str, Any] manager: base.RESTManager @cli.register_custom_action( ("ProjectIssue", "ProjectMergeRequest", "ProjectLabel", "GroupLabel") ) @exc.on_http_error(exc.GitlabSubscribeError) def subscribe(self, **kwargs: Any) -> None: """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) if TYPE_CHECKING: assert not isinstance(server_data, requests.Response) self._update_attrs(server_data) @cli.register_custom_action( ("ProjectIssue", "ProjectMergeRequest", "ProjectLabel", "GroupLabel") ) @exc.on_http_error(exc.GitlabUnsubscribeError) def unsubscribe(self, **kwargs: Any) -> None: """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) if TYPE_CHECKING: assert not isinstance(server_data, requests.Response) self._update_attrs(server_data) class TodoMixin(_RestObjectBase): _id_attr: Optional[str] _attrs: Dict[str, Any] _module: ModuleType _parent_attrs: Dict[str, Any] _updated_attrs: Dict[str, Any] manager: base.RESTManager @cli.register_custom_action(("ProjectIssue", "ProjectMergeRequest")) @exc.on_http_error(exc.GitlabTodoError) def todo(self, **kwargs: Any) -> None: """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(_RestObjectBase): _id_attr: Optional[str] _attrs: Dict[str, Any] _module: ModuleType _parent_attrs: Dict[str, Any] _updated_attrs: Dict[str, Any] manager: base.RESTManager @cli.register_custom_action(("ProjectIssue", "ProjectMergeRequest")) @exc.on_http_error(exc.GitlabTimeTrackingError) def time_stats(self, **kwargs: Any) -> Dict[str, Any]: """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()) result = self.manager.gitlab.http_get(path, **kwargs) if TYPE_CHECKING: assert not isinstance(result, requests.Response) return result @cli.register_custom_action(("ProjectIssue", "ProjectMergeRequest"), ("duration",)) @exc.on_http_error(exc.GitlabTimeTrackingError) def time_estimate(self, duration: str, **kwargs: Any) -> Dict[str, Any]: """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} result = self.manager.gitlab.http_post(path, post_data=data, **kwargs) if TYPE_CHECKING: assert not isinstance(result, requests.Response) return result @cli.register_custom_action(("ProjectIssue", "ProjectMergeRequest")) @exc.on_http_error(exc.GitlabTimeTrackingError) def reset_time_estimate(self, **kwargs: Any) -> Dict[str, Any]: """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()) result = self.manager.gitlab.http_post(path, **kwargs) if TYPE_CHECKING: assert not isinstance(result, requests.Response) return result @cli.register_custom_action(("ProjectIssue", "ProjectMergeRequest"), ("duration",)) @exc.on_http_error(exc.GitlabTimeTrackingError) def add_spent_time(self, duration: str, **kwargs: Any) -> Dict[str, Any]: """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} result = self.manager.gitlab.http_post(path, post_data=data, **kwargs) if TYPE_CHECKING: assert not isinstance(result, requests.Response) return result @cli.register_custom_action(("ProjectIssue", "ProjectMergeRequest")) @exc.on_http_error(exc.GitlabTimeTrackingError) def reset_spent_time(self, **kwargs: Any) -> Dict[str, Any]: """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()) result = self.manager.gitlab.http_post(path, **kwargs) if TYPE_CHECKING: assert not isinstance(result, requests.Response) return result class ParticipantsMixin(_RestObjectBase): _id_attr: Optional[str] _attrs: Dict[str, Any] _module: ModuleType _parent_attrs: Dict[str, Any] _updated_attrs: Dict[str, Any] manager: base.RESTManager @cli.register_custom_action(("ProjectMergeRequest", "ProjectIssue")) @exc.on_http_error(exc.GitlabListError) def participants(self, **kwargs: Any) -> Dict[str, Any]: """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()) result = self.manager.gitlab.http_get(path, **kwargs) if TYPE_CHECKING: assert not isinstance(result, requests.Response) return result class BadgeRenderMixin(_RestManagerBase): @cli.register_custom_action( ("GroupBadgeManager", "ProjectBadgeManager"), ("link_url", "image_url") ) @exc.on_http_error(exc.GitlabRenderError) def render(self, link_url: str, image_url: str, **kwargs: Any) -> Dict[str, Any]: """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} result = self.gitlab.http_get(path, data, **kwargs) if TYPE_CHECKING: assert not isinstance(result, requests.Response) return result class MemberAllMixin(_RestManagerBase): """This mixin is deprecated.""" _computed_path: Optional[str] _from_parent_attrs: Dict[str, Any] _obj_cls: Optional[Type[base.RESTObject]] _parent: Optional[base.RESTObject] _parent_attrs: Dict[str, Any] _path: Optional[str] gitlab: gitlab.Gitlab @cli.register_custom_action(("GroupMemberManager", "ProjectMemberManager")) @exc.on_http_error(exc.GitlabListError) def all(self, **kwargs: Any) -> List[base.RESTObject]: """List all the members, included inherited ones. This Method is deprecated. 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 """ warnings.warn( "The all() method for this object is deprecated " "and will be removed in a future version. Use .members_all.list(all=True), instead.", DeprecationWarning, ) path = "%s/all" % self.path if TYPE_CHECKING: assert self._obj_cls is not None obj = self.gitlab.http_list(path, **kwargs) return [self._obj_cls(self, item) for item in obj] python-gitlab-2.10.1/gitlab/py.typed000066400000000000000000000000001416141341200173000ustar00rootroot00000000000000python-gitlab-2.10.1/gitlab/types.py000066400000000000000000000040511416141341200173310ustar00rootroot00000000000000# -*- 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 . from typing import Any, Optional, TYPE_CHECKING class GitlabAttribute(object): def __init__(self, value: Any = None) -> None: self._value = value def get(self) -> Any: return self._value def set_from_cli(self, cli_value: Any) -> None: self._value = cli_value def get_for_api(self) -> Any: return self._value class ListAttribute(GitlabAttribute): def set_from_cli(self, cli_value: str) -> None: if not cli_value.strip(): self._value = [] else: self._value = [item.strip() for item in cli_value.split(",")] def get_for_api(self) -> str: # Do not comma-split single value passed as string if isinstance(self._value, str): return self._value if TYPE_CHECKING: assert isinstance(self._value, list) return ",".join([str(x) for x in self._value]) class LowercaseStringAttribute(GitlabAttribute): def get_for_api(self) -> str: return str(self._value).lower() class FileAttribute(GitlabAttribute): def get_file_name(self, attr_name: Optional[str] = None) -> Optional[str]: return attr_name class ImageAttribute(FileAttribute): def get_file_name(self, attr_name: Optional[str] = None) -> str: return "%s.png" % attr_name if attr_name else "image.png" python-gitlab-2.10.1/gitlab/utils.py000066400000000000000000000041311416141341200173240ustar00rootroot00000000000000# -*- 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 typing import Any, Callable, Dict, Optional from urllib.parse import quote, urlparse import requests class _StdoutStream(object): def __call__(self, chunk: Any) -> None: print(chunk) def response_content( response: requests.Response, streamed: bool, action: Optional[Callable], chunk_size: int, ) -> Optional[bytes]: 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) return None def copy_dict(dest: Dict[str, Any], src: Dict[str, Any]) -> None: 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: str) -> str: return quote(id, safe="") def sanitized_url(url: str) -> str: parsed = urlparse(url) new_path = parsed.path.replace(".", "%2E") return parsed._replace(path=new_path).geturl() def remove_none_from_dict(data: Dict[str, Any]) -> Dict[str, Any]: return {k: v for k, v in data.items() if v is not None} python-gitlab-2.10.1/gitlab/v4/000077500000000000000000000000001416141341200161445ustar00rootroot00000000000000python-gitlab-2.10.1/gitlab/v4/__init__.py000066400000000000000000000000001416141341200202430ustar00rootroot00000000000000python-gitlab-2.10.1/gitlab/v4/cli.py000066400000000000000000000431611416141341200172720ustar00rootroot00000000000000#!/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 . import argparse import operator import sys from typing import Any, Dict, List, Optional, Type, TYPE_CHECKING, Union import gitlab import gitlab.base import gitlab.v4.objects from gitlab import cli class GitlabCLI(object): def __init__( self, gl: gitlab.Gitlab, what: str, action: str, args: Dict[str, str] ) -> None: self.cls: Type[gitlab.base.RESTObject] = cli.what_to_cls( what, namespace=gitlab.v4.objects ) self.cls_name = self.cls.__name__ self.what = what.replace("-", "_") self.action = action.lower() self.gl = gl self.args = args self.mgr_cls: Union[ Type[gitlab.mixins.CreateMixin], Type[gitlab.mixins.DeleteMixin], Type[gitlab.mixins.GetMixin], Type[gitlab.mixins.GetWithoutIdMixin], Type[gitlab.mixins.ListMixin], Type[gitlab.mixins.UpdateMixin], ] = 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. if TYPE_CHECKING: assert self.mgr_cls._path is not None self.mgr_cls._path = self.mgr_cls._path % self.args self.mgr = self.mgr_cls(gl) if self.mgr_cls._types: for attr_name, type_cls in self.mgr_cls._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) -> Any: # 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) -> Any: in_obj = cli.custom_actions[self.cls_name][self.action][2] # Get the object (lazy), then act if in_obj: data = {} if self.mgr._from_parent_attrs: for k in self.mgr._from_parent_attrs: data[k] = self.args[k] if not issubclass(self.cls, gitlab.mixins.GetWithoutIdMixin): if TYPE_CHECKING: assert isinstance(self.cls._id_attr, str) data[self.cls._id_attr] = self.args.pop(self.cls._id_attr) obj = self.cls(self.mgr, data) method_name = self.action.replace("-", "_") return getattr(obj, method_name)(**self.args) else: return getattr(self.mgr, self.action)(**self.args) def do_project_export_download(self) -> None: try: project = self.gl.projects.get(int(self.args["project_id"]), lazy=True) data = project.exports.get().download() sys.stdout.buffer.write(data) except Exception as e: cli.die("Impossible to download the export", e) def do_create(self) -> gitlab.base.RESTObject: if TYPE_CHECKING: assert isinstance(self.mgr, gitlab.mixins.CreateMixin) try: result = self.mgr.create(self.args) except Exception as e: cli.die("Impossible to create object", e) return result def do_list( self, ) -> Union[gitlab.base.RESTObjectList, List[gitlab.base.RESTObject]]: if TYPE_CHECKING: assert isinstance(self.mgr, gitlab.mixins.ListMixin) try: result = self.mgr.list(**self.args) except Exception as e: cli.die("Impossible to list objects", e) return result def do_get(self) -> Optional[gitlab.base.RESTObject]: if isinstance(self.mgr, gitlab.mixins.GetWithoutIdMixin): try: result = self.mgr.get(id=None, **self.args) except Exception as e: cli.die("Impossible to get object", e) return result if TYPE_CHECKING: assert isinstance(self.mgr, gitlab.mixins.GetMixin) assert isinstance(self.cls._id_attr, str) id = self.args.pop(self.cls._id_attr) try: result = self.mgr.get(id, lazy=False, **self.args) except Exception as e: cli.die("Impossible to get object", e) return result def do_delete(self) -> None: if TYPE_CHECKING: assert isinstance(self.mgr, gitlab.mixins.DeleteMixin) assert isinstance(self.cls._id_attr, str) 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) -> Dict[str, Any]: if TYPE_CHECKING: assert isinstance(self.mgr, gitlab.mixins.UpdateMixin) if issubclass(self.mgr_cls, gitlab.mixins.GetWithoutIdMixin): id = None else: if TYPE_CHECKING: assert isinstance(self.cls._id_attr, str) id = self.args.pop(self.cls._id_attr) try: result = self.mgr.update(id, self.args) except Exception as e: cli.die("Impossible to update object", e) return result def _populate_sub_parser_by_class( cls: Type[gitlab.base.RESTObject], sub_parser: argparse._SubParsersAction ) -> None: 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 mgr_cls._from_parent_attrs: for x in mgr_cls._from_parent_attrs: sub_parser_action.add_argument( "--%s" % x.replace("_", "-"), required=True ) if action_name == "list": for x in mgr_cls._list_filters: sub_parser_action.add_argument( "--%s" % x.replace("_", "-"), required=False ) 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 not issubclass(cls, gitlab.mixins.GetWithoutIdMixin): if cls._id_attr is not None: id_attr = cls._id_attr.replace("_", "-") sub_parser_action.add_argument("--%s" % id_attr, required=True) for x in mgr_cls._optional_get_attrs: sub_parser_action.add_argument( "--%s" % x.replace("_", "-"), required=False ) if action_name == "create": for x in mgr_cls._create_attrs.required: sub_parser_action.add_argument( "--%s" % x.replace("_", "-"), required=True ) for x in mgr_cls._create_attrs.optional: sub_parser_action.add_argument( "--%s" % x.replace("_", "-"), required=False ) 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) for x in mgr_cls._update_attrs.required: if x != cls._id_attr: sub_parser_action.add_argument( "--%s" % x.replace("_", "-"), required=True ) for x in mgr_cls._update_attrs.optional: if x != cls._id_attr: sub_parser_action.add_argument( "--%s" % x.replace("_", "-"), required=False ) 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 mgr_cls._from_parent_attrs: for x in mgr_cls._from_parent_attrs: sub_parser_action.add_argument( "--%s" % x.replace("_", "-"), required=True ) sub_parser_action.add_argument("--sudo", required=False) # We need to get the object somehow if not issubclass(cls, gitlab.mixins.GetWithoutIdMixin): 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 mgr_cls._from_parent_attrs: for x in mgr_cls._from_parent_attrs: sub_parser_action.add_argument( "--%s" % x.replace("_", "-"), required=True ) 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: argparse.ArgumentParser) -> argparse.ArgumentParser: 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(): if not isinstance(cls, type): continue if issubclass(cls, gitlab.base.RESTManager): if cls._obj_cls is not None: classes.append(cls._obj_cls) 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: Union[str, gitlab.base.RESTObject], fields: List[str] ) -> Union[str, Dict[str, Any]]: 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: Union[str, Dict[str, Any]], **kwargs: Any) -> None: import json # noqa print(json.dumps(d)) def display_list( self, data: List[Union[str, gitlab.base.RESTObject]], fields: List[str], **kwargs: Any ) -> None: import json # noqa print(json.dumps([get_dict(obj, fields) for obj in data])) class YAMLPrinter(object): def display(self, d: Union[str, Dict[str, Any]], **kwargs: Any) -> None: 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: List[Union[str, gitlab.base.RESTObject]], fields: List[str], **kwargs: Any ) -> None: 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: Union[str, Dict[str, Any]], **kwargs: Any) -> None: verbose = kwargs.get("verbose", False) padding = kwargs.get("padding", 0) obj: Optional[Union[Dict[str, Any], gitlab.base.RESTObject]] = kwargs.get("obj") if TYPE_CHECKING: assert obj is not None def display_dict(d: Dict[str, Any], padding: int) -> None: 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 TYPE_CHECKING: assert isinstance(obj, gitlab.base.RESTObject) if obj._id_attr: id = getattr(obj, obj._id_attr) print("%s: %s" % (obj._id_attr.replace("_", "-"), id)) if 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: List[Union[str, gitlab.base.RESTObject]], fields: List[str], **kwargs: Any ) -> None: 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: Dict[ str, Union[Type[JSONPrinter], Type[LegacyPrinter], Type[YAMLPrinter]] ] = { "json": JSONPrinter, "legacy": LegacyPrinter, "yaml": YAMLPrinter, } def run( gl: gitlab.Gitlab, what: str, action: str, args: Dict[str, Any], verbose: bool, output: str, fields: List[str], ) -> None: g_cli = GitlabCLI(gl=gl, what=what, action=action, args=args) data = g_cli() printer: Union[JSONPrinter, LegacyPrinter, YAMLPrinter] = 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 isinstance(data, bytes): sys.stdout.buffer.write(data) elif hasattr(data, "decode"): print(data.decode()) python-gitlab-2.10.1/gitlab/v4/objects/000077500000000000000000000000001416141341200175755ustar00rootroot00000000000000python-gitlab-2.10.1/gitlab/v4/objects/__init__.py000066400000000000000000000047371416141341200217210ustar00rootroot00000000000000# -*- 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 .access_requests import * from .appearance import * from .applications import * from .audit_events import * from .award_emojis import * from .badges import * from .boards import * from .branches import * from .broadcast_messages import * from .clusters import * from .commits import * from .container_registry import * from .custom_attributes import * from .deploy_keys import * from .deploy_tokens import * from .deployments import * from .discussions import * from .environments import * from .epics import * from .events import * from .export_import import * from .features import * from .files import * from .geo_nodes import * from .groups import * from .hooks import * from .issues import * from .jobs import * from .keys import * from .labels import * from .ldap import * from .members import * from .merge_request_approvals import * from .merge_requests import * from .milestones import * from .namespaces import * from .notes import * from .notification_settings import * from .packages import * from .pages import * from .personal_access_tokens import * from .pipelines import * from .projects import * from .push_rules import * from .releases import * from .runners import * from .services import * from .settings import * from .sidekiq import * from .snippets import * from .statistics import * from .tags import * from .templates import * from .todos import * from .triggers import * from .users import * from .variables import * from .wikis import * # TODO: deprecate these in favor of gitlab.const.* VISIBILITY_PRIVATE = "private" VISIBILITY_INTERNAL = "internal" VISIBILITY_PUBLIC = "public" ACCESS_GUEST = 10 ACCESS_REPORTER = 20 ACCESS_DEVELOPER = 30 ACCESS_MASTER = 40 ACCESS_OWNER = 50 __all__ = [name for name in dir() if not name.startswith("_")] python-gitlab-2.10.1/gitlab/v4/objects/access_requests.py000066400000000000000000000016371416141341200233520ustar00rootroot00000000000000from gitlab.base import RESTManager, RESTObject from gitlab.mixins import ( AccessRequestMixin, CreateMixin, DeleteMixin, ListMixin, ObjectDeleteMixin, ) __all__ = [ "GroupAccessRequest", "GroupAccessRequestManager", "ProjectAccessRequest", "ProjectAccessRequestManager", ] 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 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"} python-gitlab-2.10.1/gitlab/v4/objects/appearance.py000066400000000000000000000031531416141341200222500ustar00rootroot00000000000000from gitlab import exceptions as exc from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import GetWithoutIdMixin, SaveMixin, UpdateMixin __all__ = [ "ApplicationAppearance", "ApplicationAppearanceManager", ] class ApplicationAppearance(SaveMixin, RESTObject): _id_attr = None class ApplicationAppearanceManager(GetWithoutIdMixin, UpdateMixin, RESTManager): _path = "/application/appearance" _obj_cls = ApplicationAppearance _update_attrs = RequiredOptional( optional=( "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) python-gitlab-2.10.1/gitlab/v4/objects/applications.py000066400000000000000000000010761416141341200226410ustar00rootroot00000000000000from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import CreateMixin, DeleteMixin, ListMixin, ObjectDeleteMixin __all__ = [ "Application", "ApplicationManager", ] class Application(ObjectDeleteMixin, RESTObject): _url = "/applications" _short_print_attr = "name" class ApplicationManager(ListMixin, CreateMixin, DeleteMixin, RESTManager): _path = "/applications" _obj_cls = Application _create_attrs = RequiredOptional( required=("name", "redirect_uri", "scopes"), optional=("confidential",) ) python-gitlab-2.10.1/gitlab/v4/objects/audit_events.py000066400000000000000000000024671416141341200226520ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ee/api/audit_events.html """ from gitlab.base import RESTManager, RESTObject from gitlab.mixins import RetrieveMixin __all__ = [ "AuditEvent", "AuditEventManager", "GroupAuditEvent", "GroupAuditEventManager", "ProjectAuditEvent", "ProjectAuditEventManager", "ProjectAudit", "ProjectAuditManager", ] class AuditEvent(RESTObject): _id_attr = "id" class AuditEventManager(RetrieveMixin, RESTManager): _path = "/audit_events" _obj_cls = AuditEvent _list_filters = ("created_after", "created_before", "entity_type", "entity_id") class GroupAuditEvent(RESTObject): _id_attr = "id" class GroupAuditEventManager(RetrieveMixin, RESTManager): _path = "/groups/%(group_id)s/audit_events" _obj_cls = GroupAuditEvent _from_parent_attrs = {"group_id": "id"} _list_filters = ("created_after", "created_before") class ProjectAuditEvent(RESTObject): _id_attr = "id" class ProjectAuditEventManager(RetrieveMixin, RESTManager): _path = "/projects/%(project_id)s/audit_events" _obj_cls = ProjectAuditEvent _from_parent_attrs = {"project_id": "id"} _list_filters = ("created_after", "created_before") class ProjectAudit(ProjectAuditEvent): pass class ProjectAuditManager(ProjectAuditEventManager): pass python-gitlab-2.10.1/gitlab/v4/objects/award_emojis.py000066400000000000000000000063061416141341200226200ustar00rootroot00000000000000from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import NoUpdateMixin, ObjectDeleteMixin __all__ = [ "ProjectIssueAwardEmoji", "ProjectIssueAwardEmojiManager", "ProjectIssueNoteAwardEmoji", "ProjectIssueNoteAwardEmojiManager", "ProjectMergeRequestAwardEmoji", "ProjectMergeRequestAwardEmojiManager", "ProjectMergeRequestNoteAwardEmoji", "ProjectMergeRequestNoteAwardEmojiManager", "ProjectSnippetAwardEmoji", "ProjectSnippetAwardEmojiManager", "ProjectSnippetNoteAwardEmoji", "ProjectSnippetNoteAwardEmojiManager", ] 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 = RequiredOptional(required=("name",)) 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 = RequiredOptional(required=("name",)) 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 = RequiredOptional(required=("name",)) 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 = RequiredOptional(required=("name",)) 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 = RequiredOptional(required=("name",)) 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 = RequiredOptional(required=("name",)) python-gitlab-2.10.1/gitlab/v4/objects/badges.py000066400000000000000000000020471416141341200213770ustar00rootroot00000000000000from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import BadgeRenderMixin, CRUDMixin, ObjectDeleteMixin, SaveMixin __all__ = [ "GroupBadge", "GroupBadgeManager", "ProjectBadge", "ProjectBadgeManager", ] 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 = RequiredOptional(required=("link_url", "image_url")) _update_attrs = RequiredOptional(optional=("link_url", "image_url")) 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 = RequiredOptional(required=("link_url", "image_url")) _update_attrs = RequiredOptional(optional=("link_url", "image_url")) python-gitlab-2.10.1/gitlab/v4/objects/boards.py000066400000000000000000000035111416141341200214210ustar00rootroot00000000000000from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import CRUDMixin, ObjectDeleteMixin, SaveMixin __all__ = [ "GroupBoardList", "GroupBoardListManager", "GroupBoard", "GroupBoardManager", "ProjectBoardList", "ProjectBoardListManager", "ProjectBoard", "ProjectBoardManager", ] 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 = RequiredOptional(required=("label_id",)) _update_attrs = RequiredOptional(required=("position",)) 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 = RequiredOptional(required=("name",)) 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 = RequiredOptional(required=("label_id",)) _update_attrs = RequiredOptional(required=("position",)) 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 = RequiredOptional(required=("name",)) python-gitlab-2.10.1/gitlab/v4/objects/branches.py000066400000000000000000000060411416141341200217350ustar00rootroot00000000000000from gitlab import cli from gitlab import exceptions as exc from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import NoUpdateMixin, ObjectDeleteMixin __all__ = [ "ProjectBranch", "ProjectBranchManager", "ProjectProtectedBranch", "ProjectProtectedBranchManager", ] 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 = RequiredOptional(required=("branch", "ref")) 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 = RequiredOptional( required=("name",), optional=( "push_access_level", "merge_access_level", "unprotect_access_level", "allowed_to_push", "allowed_to_merge", "allowed_to_unprotect", "code_owner_approval_required", ), ) python-gitlab-2.10.1/gitlab/v4/objects/broadcast_messages.py000066400000000000000000000012031416141341200237740ustar00rootroot00000000000000from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import CRUDMixin, ObjectDeleteMixin, SaveMixin __all__ = [ "BroadcastMessage", "BroadcastMessageManager", ] class BroadcastMessage(SaveMixin, ObjectDeleteMixin, RESTObject): pass class BroadcastMessageManager(CRUDMixin, RESTManager): _path = "/broadcast_messages" _obj_cls = BroadcastMessage _create_attrs = RequiredOptional( required=("message",), optional=("starts_at", "ends_at", "color", "font") ) _update_attrs = RequiredOptional( optional=("message", "starts_at", "ends_at", "color", "font") ) python-gitlab-2.10.1/gitlab/v4/objects/clusters.py000066400000000000000000000061711416141341200220200ustar00rootroot00000000000000from gitlab import exceptions as exc from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import CreateMixin, CRUDMixin, ObjectDeleteMixin, SaveMixin __all__ = [ "GroupCluster", "GroupClusterManager", "ProjectCluster", "ProjectClusterManager", ] 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 = RequiredOptional( required=("name", "platform_kubernetes_attributes"), optional=("domain", "enabled", "managed", "environment_scope"), ) _update_attrs = RequiredOptional( optional=( "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 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 = RequiredOptional( required=("name", "platform_kubernetes_attributes"), optional=("domain", "enabled", "managed", "environment_scope"), ) _update_attrs = RequiredOptional( optional=( "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) python-gitlab-2.10.1/gitlab/v4/objects/commits.py000066400000000000000000000162021416141341200216230ustar00rootroot00000000000000from gitlab import cli from gitlab import exceptions as exc from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import CreateMixin, ListMixin, RefreshMixin, RetrieveMixin from .discussions import ProjectCommitDiscussionManager # noqa: F401 __all__ = [ "ProjectCommit", "ProjectCommitManager", "ProjectCommitComment", "ProjectCommitCommentManager", "ProjectCommitStatus", "ProjectCommitStatusManager", ] 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) @cli.register_custom_action("ProjectCommit", ("branch",)) @exc.on_http_error(exc.GitlabRevertError) def revert(self, branch, **kwargs): """Revert a commit on a given 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 GitlabRevertError: If the revert could not be performed Returns: dict: The new commit data (*not* a RESTObject) """ path = "%s/%s/revert" % (self.manager.path, self.get_id()) post_data = {"branch": branch} return self.manager.gitlab.http_post(path, post_data=post_data, **kwargs) @cli.register_custom_action("ProjectCommit") @exc.on_http_error(exc.GitlabGetError) def signature(self, **kwargs): """Get the signature of the commit. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabGetError: If the signature could not be retrieved Returns: dict: The commit's signature data """ path = "%s/%s/signature" % (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 = RequiredOptional( required=("branch", "commit_message", "actions"), optional=("author_email", "author_name"), ) 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 = RequiredOptional( required=("note",), optional=("path", "line", "line_type") ) class ProjectCommitStatus(RefreshMixin, RESTObject): 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 = RequiredOptional( required=("state",), optional=("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) python-gitlab-2.10.1/gitlab/v4/objects/container_registry.py000066400000000000000000000045161416141341200240670ustar00rootroot00000000000000from gitlab import cli from gitlab import exceptions as exc from gitlab.base import RESTManager, RESTObject from gitlab.mixins import DeleteMixin, ListMixin, ObjectDeleteMixin, RetrieveMixin __all__ = [ "ProjectRegistryRepository", "ProjectRegistryRepositoryManager", "ProjectRegistryTag", "ProjectRegistryTagManager", ] 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. name_regex_keep (string): The regex of the name to keep. This value overrides any matches from name_regex. 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", "name_regex_keep", "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) python-gitlab-2.10.1/gitlab/v4/objects/custom_attributes.py000066400000000000000000000023431416141341200237310ustar00rootroot00000000000000from gitlab.base import RESTManager, RESTObject from gitlab.mixins import DeleteMixin, ObjectDeleteMixin, RetrieveMixin, SetMixin __all__ = [ "GroupCustomAttribute", "GroupCustomAttributeManager", "ProjectCustomAttribute", "ProjectCustomAttributeManager", "UserCustomAttribute", "UserCustomAttributeManager", ] 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 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 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"} python-gitlab-2.10.1/gitlab/v4/objects/deploy_keys.py000066400000000000000000000026721416141341200225050ustar00rootroot00000000000000from gitlab import cli from gitlab import exceptions as exc from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import CRUDMixin, ListMixin, ObjectDeleteMixin, SaveMixin __all__ = [ "DeployKey", "DeployKeyManager", "ProjectKey", "ProjectKeyManager", ] class DeployKey(RESTObject): pass class DeployKeyManager(ListMixin, RESTManager): _path = "/deploy_keys" _obj_cls = DeployKey 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 = RequiredOptional(required=("title", "key"), optional=("can_push",)) _update_attrs = RequiredOptional(optional=("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) python-gitlab-2.10.1/gitlab/v4/objects/deploy_tokens.py000066400000000000000000000030251416141341200230260ustar00rootroot00000000000000from gitlab import types from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import CreateMixin, DeleteMixin, ListMixin, ObjectDeleteMixin __all__ = [ "DeployToken", "DeployTokenManager", "GroupDeployToken", "GroupDeployTokenManager", "ProjectDeployToken", "ProjectDeployTokenManager", ] class DeployToken(ObjectDeleteMixin, RESTObject): pass class DeployTokenManager(ListMixin, RESTManager): _path = "/deploy_tokens" _obj_cls = DeployToken class GroupDeployToken(ObjectDeleteMixin, RESTObject): pass class GroupDeployTokenManager(ListMixin, CreateMixin, DeleteMixin, RESTManager): _path = "/groups/%(group_id)s/deploy_tokens" _from_parent_attrs = {"group_id": "id"} _obj_cls = GroupDeployToken _create_attrs = RequiredOptional( required=( "name", "scopes", ), optional=( "expires_at", "username", ), ) _types = {"scopes": types.ListAttribute} class ProjectDeployToken(ObjectDeleteMixin, RESTObject): pass class ProjectDeployTokenManager(ListMixin, CreateMixin, DeleteMixin, RESTManager): _path = "/projects/%(project_id)s/deploy_tokens" _from_parent_attrs = {"project_id": "id"} _obj_cls = ProjectDeployToken _create_attrs = RequiredOptional( required=( "name", "scopes", ), optional=( "expires_at", "username", ), ) _types = {"scopes": types.ListAttribute} python-gitlab-2.10.1/gitlab/v4/objects/deployments.py000066400000000000000000000016101416141341200225100ustar00rootroot00000000000000from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import CreateMixin, RetrieveMixin, SaveMixin, UpdateMixin from .merge_requests import ProjectDeploymentMergeRequestManager # noqa: F401 __all__ = [ "ProjectDeployment", "ProjectDeploymentManager", ] class ProjectDeployment(SaveMixin, RESTObject): _managers = (("mergerequests", "ProjectDeploymentMergeRequestManager"),) 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", "updated_after", "updated_before", "environment", "status", ) _create_attrs = RequiredOptional( required=("sha", "ref", "tag", "status", "environment") ) python-gitlab-2.10.1/gitlab/v4/objects/discussions.py000066400000000000000000000051351416141341200225210ustar00rootroot00000000000000from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import CreateMixin, RetrieveMixin, SaveMixin, UpdateMixin from .notes import ( # noqa: F401 ProjectCommitDiscussionNoteManager, ProjectIssueDiscussionNoteManager, ProjectMergeRequestDiscussionNoteManager, ProjectSnippetDiscussionNoteManager, ) __all__ = [ "ProjectCommitDiscussion", "ProjectCommitDiscussionManager", "ProjectIssueDiscussion", "ProjectIssueDiscussionManager", "ProjectMergeRequestDiscussion", "ProjectMergeRequestDiscussionManager", "ProjectSnippetDiscussion", "ProjectSnippetDiscussionManager", ] 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 = RequiredOptional(required=("body",), optional=("created_at",)) 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 = RequiredOptional(required=("body",), optional=("created_at",)) 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 = RequiredOptional( required=("body",), optional=("created_at", "position") ) _update_attrs = RequiredOptional(required=("resolved",)) 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 = RequiredOptional(required=("body",), optional=("created_at",)) python-gitlab-2.10.1/gitlab/v4/objects/environments.py000066400000000000000000000024541416141341200227030ustar00rootroot00000000000000from gitlab import cli from gitlab import exceptions as exc from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import ( CreateMixin, DeleteMixin, ObjectDeleteMixin, RetrieveMixin, SaveMixin, UpdateMixin, ) __all__ = [ "ProjectEnvironment", "ProjectEnvironmentManager", ] 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 = RequiredOptional(required=("name",), optional=("external_url",)) _update_attrs = RequiredOptional(optional=("name", "external_url")) python-gitlab-2.10.1/gitlab/v4/objects/epics.py000066400000000000000000000066601416141341200212620ustar00rootroot00000000000000from gitlab import exceptions as exc from gitlab import types from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import ( CreateMixin, CRUDMixin, DeleteMixin, ListMixin, ObjectDeleteMixin, SaveMixin, UpdateMixin, ) from .events import GroupEpicResourceLabelEventManager # noqa: F401 __all__ = [ "GroupEpic", "GroupEpicManager", "GroupEpicIssue", "GroupEpicIssueManager", ] 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 = RequiredOptional( required=("title",), optional=("labels", "description", "start_date", "end_date"), ) _update_attrs = RequiredOptional( optional=("title", "labels", "description", "start_date", "end_date"), ) _types = {"labels": types.ListAttribute} 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 = RequiredOptional(required=("issue_id",)) _update_attrs = RequiredOptional(optional=("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) python-gitlab-2.10.1/gitlab/v4/objects/events.py000066400000000000000000000076161416141341200214650ustar00rootroot00000000000000from gitlab.base import RESTManager, RESTObject from gitlab.mixins import ListMixin, RetrieveMixin __all__ = [ "Event", "EventManager", "GroupEpicResourceLabelEvent", "GroupEpicResourceLabelEventManager", "ProjectEvent", "ProjectEventManager", "ProjectIssueResourceLabelEvent", "ProjectIssueResourceLabelEventManager", "ProjectIssueResourceMilestoneEvent", "ProjectIssueResourceMilestoneEventManager", "ProjectIssueResourceStateEvent", "ProjectIssueResourceStateEventManager", "ProjectMergeRequestResourceLabelEvent", "ProjectMergeRequestResourceLabelEventManager", "ProjectMergeRequestResourceMilestoneEvent", "ProjectMergeRequestResourceMilestoneEventManager", "ProjectMergeRequestResourceStateEvent", "ProjectMergeRequestResourceStateEventManager", "UserEvent", "UserEventManager", ] class Event(RESTObject): _id_attr = None _short_print_attr = "target_title" class EventManager(ListMixin, RESTManager): _path = "/events" _obj_cls = Event _list_filters = ("action", "target_type", "before", "after", "sort") class 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 ProjectEvent(Event): pass class ProjectEventManager(EventManager): _path = "/projects/%(project_id)s/events" _obj_cls = ProjectEvent _from_parent_attrs = {"project_id": "id"} 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 ProjectIssueResourceMilestoneEvent(RESTObject): pass class ProjectIssueResourceMilestoneEventManager(RetrieveMixin, RESTManager): _path = "/projects/%(project_id)s/issues/%(issue_iid)s/resource_milestone_events" _obj_cls = ProjectIssueResourceMilestoneEvent _from_parent_attrs = {"project_id": "project_id", "issue_iid": "iid"} class ProjectIssueResourceStateEvent(RESTObject): pass class ProjectIssueResourceStateEventManager(RetrieveMixin, RESTManager): _path = "/projects/%(project_id)s/issues/%(issue_iid)s/resource_state_events" _obj_cls = ProjectIssueResourceStateEvent _from_parent_attrs = {"project_id": "project_id", "issue_iid": "iid"} 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 ProjectMergeRequestResourceMilestoneEvent(RESTObject): pass class ProjectMergeRequestResourceMilestoneEventManager(RetrieveMixin, RESTManager): _path = ( "/projects/%(project_id)s/merge_requests/%(mr_iid)s/resource_milestone_events" ) _obj_cls = ProjectMergeRequestResourceMilestoneEvent _from_parent_attrs = {"project_id": "project_id", "mr_iid": "iid"} class ProjectMergeRequestResourceStateEvent(RESTObject): pass class ProjectMergeRequestResourceStateEventManager(RetrieveMixin, RESTManager): _path = "/projects/%(project_id)s/merge_requests/%(mr_iid)s/resource_state_events" _obj_cls = ProjectMergeRequestResourceStateEvent _from_parent_attrs = {"project_id": "project_id", "mr_iid": "iid"} class UserEvent(Event): pass class UserEventManager(EventManager): _path = "/users/%(user_id)s/events" _obj_cls = UserEvent _from_parent_attrs = {"user_id": "id"} python-gitlab-2.10.1/gitlab/v4/objects/export_import.py000066400000000000000000000026271416141341200230710ustar00rootroot00000000000000from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import CreateMixin, DownloadMixin, GetWithoutIdMixin, RefreshMixin __all__ = [ "GroupExport", "GroupExportManager", "GroupImport", "GroupImportManager", "ProjectExport", "ProjectExportManager", "ProjectImport", "ProjectImportManager", ] class GroupExport(DownloadMixin, RESTObject): _id_attr = None class GroupExportManager(GetWithoutIdMixin, CreateMixin, RESTManager): _path = "/groups/%(group_id)s/export" _obj_cls = GroupExport _from_parent_attrs = {"group_id": "id"} class GroupImport(RESTObject): _id_attr = None class GroupImportManager(GetWithoutIdMixin, RESTManager): _path = "/groups/%(group_id)s/import" _obj_cls = GroupImport _from_parent_attrs = {"group_id": "id"} class ProjectExport(DownloadMixin, RefreshMixin, RESTObject): _id_attr = None class ProjectExportManager(GetWithoutIdMixin, CreateMixin, RESTManager): _path = "/projects/%(project_id)s/export" _obj_cls = ProjectExport _from_parent_attrs = {"project_id": "id"} _create_attrs = RequiredOptional(optional=("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"} python-gitlab-2.10.1/gitlab/v4/objects/features.py000066400000000000000000000032461416141341200217720ustar00rootroot00000000000000from gitlab import exceptions as exc from gitlab import utils from gitlab.base import RESTManager, RESTObject from gitlab.mixins import DeleteMixin, ListMixin, ObjectDeleteMixin __all__ = [ "Feature", "FeatureManager", ] 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, group=None, project=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 group (str): A GitLab group project (str): A GitLab project in form group/project **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, "group": group, "project": project, } data = utils.remove_none_from_dict(data) server_data = self.gitlab.http_post(path, post_data=data, **kwargs) return self._obj_cls(self, server_data) python-gitlab-2.10.1/gitlab/v4/objects/files.py000066400000000000000000000204741416141341200212600ustar00rootroot00000000000000import base64 from gitlab import cli from gitlab import exceptions as exc from gitlab import utils from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import ( CreateMixin, DeleteMixin, GetMixin, ObjectDeleteMixin, SaveMixin, UpdateMixin, ) __all__ = [ "ProjectFile", "ProjectFileManager", ] class ProjectFile(SaveMixin, ObjectDeleteMixin, RESTObject): _id_attr = "file_path" _short_print_attr = "file_path" def decode(self) -> bytes: """Returns the decoded content of the file. Returns: (bytes): 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 = RequiredOptional( required=("file_path", "branch", "content", "commit_message"), optional=("encoding", "author_email", "author_name"), ) _update_attrs = RequiredOptional( required=("file_path", "branch", "content", "commit_message"), optional=("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 """ 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) python-gitlab-2.10.1/gitlab/v4/objects/geo_nodes.py000066400000000000000000000056301416141341200221150ustar00rootroot00000000000000from gitlab import cli from gitlab import exceptions as exc from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import ( DeleteMixin, ObjectDeleteMixin, RetrieveMixin, SaveMixin, UpdateMixin, ) __all__ = [ "GeoNode", "GeoNodeManager", ] 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 = RequiredOptional( optional=("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.10.1/gitlab/v4/objects/groups.py000066400000000000000000000301421416141341200214660ustar00rootroot00000000000000from gitlab import cli from gitlab import exceptions as exc from gitlab import types from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import CRUDMixin, ListMixin, ObjectDeleteMixin, SaveMixin from .access_requests import GroupAccessRequestManager # noqa: F401 from .audit_events import GroupAuditEventManager # noqa: F401 from .badges import GroupBadgeManager # noqa: F401 from .boards import GroupBoardManager # noqa: F401 from .clusters import GroupClusterManager # noqa: F401 from .custom_attributes import GroupCustomAttributeManager # noqa: F401 from .deploy_tokens import GroupDeployTokenManager # noqa: F401 from .epics import GroupEpicManager # noqa: F401 from .export_import import GroupExportManager, GroupImportManager # noqa: F401 from .hooks import GroupHookManager # noqa: F401 from .issues import GroupIssueManager # noqa: F401 from .labels import GroupLabelManager # noqa: F401 from .members import ( # noqa: F401 GroupBillableMemberManager, GroupMemberAllManager, GroupMemberManager, ) from .merge_requests import GroupMergeRequestManager # noqa: F401 from .milestones import GroupMilestoneManager # noqa: F401 from .notification_settings import GroupNotificationSettingsManager # noqa: F401 from .packages import GroupPackageManager # noqa: F401 from .projects import GroupProjectManager # noqa: F401 from .runners import GroupRunnerManager # noqa: F401 from .statistics import GroupIssuesStatisticsManager # noqa: F401 from .variables import GroupVariableManager # noqa: F401 from .wikis import GroupWikiManager # noqa: F401 __all__ = [ "Group", "GroupManager", "GroupDescendantGroup", "GroupDescendantGroupManager", "GroupSubgroup", "GroupSubgroupManager", ] class Group(SaveMixin, ObjectDeleteMixin, RESTObject): _short_print_attr = "name" _managers = ( ("accessrequests", "GroupAccessRequestManager"), ("audit_events", "GroupAuditEventManager"), ("badges", "GroupBadgeManager"), ("billable_members", "GroupBillableMemberManager"), ("boards", "GroupBoardManager"), ("customattributes", "GroupCustomAttributeManager"), ("descendant_groups", "GroupDescendantGroupManager"), ("exports", "GroupExportManager"), ("epics", "GroupEpicManager"), ("hooks", "GroupHookManager"), ("imports", "GroupImportManager"), ("issues", "GroupIssueManager"), ("issues_statistics", "GroupIssuesStatisticsManager"), ("labels", "GroupLabelManager"), ("members", "GroupMemberManager"), ("members_all", "GroupMemberAllManager"), ("mergerequests", "GroupMergeRequestManager"), ("milestones", "GroupMilestoneManager"), ("notificationsettings", "GroupNotificationSettingsManager"), ("packages", "GroupPackageManager"), ("projects", "GroupProjectManager"), ("runners", "GroupRunnerManager"), ("subgroups", "GroupSubgroupManager"), ("variables", "GroupVariableManager"), ("clusters", "GroupClusterManager"), ("deploytokens", "GroupDeployTokenManager"), ("wikis", "GroupWikiManager"), ) @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) @cli.register_custom_action("Group", ("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 group 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 = "/groups/%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("Group", ("group_id",)) @exc.on_http_error(exc.GitlabDeleteError) def unshare(self, group_id, **kwargs): """Delete a shared group 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 = "/groups/%s/share/%s" % (self.get_id(), group_id) self.manager.gitlab.http_delete(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", "min_access_level", "top_level_only", ) _create_attrs = RequiredOptional( required=("name", "path"), optional=( "description", "membership_lock", "visibility", "share_with_group_lock", "require_two_factor_authentication", "two_factor_grace_period", "project_creation_level", "auto_devops_enabled", "subgroup_creation_level", "emails_disabled", "avatar", "mentions_disabled", "lfs_enabled", "request_access_enabled", "parent_id", "default_branch_protection", "shared_runners_minutes_limit", "extra_shared_runners_minutes_limit", ), ) _update_attrs = RequiredOptional( optional=( "name", "path", "description", "membership_lock", "share_with_group_lock", "visibility", "require_two_factor_authentication", "two_factor_grace_period", "project_creation_level", "auto_devops_enabled", "subgroup_creation_level", "emails_disabled", "avatar", "mentions_disabled", "lfs_enabled", "request_access_enabled", "default_branch_protection", "file_template_project_id", "shared_runners_minutes_limit", "extra_shared_runners_minutes_limit", "prevent_forking_outside_group", "shared_runners_setting", ), ) _types = {"avatar": types.ImageAttribute, "skip_groups": types.ListAttribute} @exc.on_http_error(exc.GitlabImportError) def import_group(self, file, path, name, parent_id=None, **kwargs): """Import a group from an archive file. Args: file: Data or file object containing the group path (str): The path for the new group to be imported. name (str): The name for the new group. parent_id (str): ID of a parent group that the group will be imported into. **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabImportError: If the server failed to perform the request Returns: dict: A representation of the import status. """ files = {"file": ("file.tar.gz", file, "application/octet-stream")} data = {"path": path, "name": name} if parent_id is not None: data["parent_id"] = parent_id return self.gitlab.http_post( "/groups/import", post_data=data, files=files, **kwargs ) 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", "min_access_level", ) _types = {"skip_groups": types.ListAttribute} class GroupDescendantGroup(RESTObject): pass class GroupDescendantGroupManager(GroupSubgroupManager): """ This manager inherits from GroupSubgroupManager as descendant groups share all attributes with subgroups, except the path and object class. """ _path = "/groups/%(group_id)s/descendant_groups" _obj_cls = GroupDescendantGroup python-gitlab-2.10.1/gitlab/v4/objects/hooks.py000066400000000000000000000057771416141341200213120ustar00rootroot00000000000000from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import CRUDMixin, NoUpdateMixin, ObjectDeleteMixin, SaveMixin __all__ = [ "Hook", "HookManager", "ProjectHook", "ProjectHookManager", "GroupHook", "GroupHookManager", ] class Hook(ObjectDeleteMixin, RESTObject): _url = "/hooks" _short_print_attr = "url" class HookManager(NoUpdateMixin, RESTManager): _path = "/hooks" _obj_cls = Hook _create_attrs = RequiredOptional(required=("url",)) 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 = RequiredOptional( required=("url",), optional=( "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 = RequiredOptional( required=("url",), optional=( "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 GroupHook(SaveMixin, ObjectDeleteMixin, RESTObject): _short_print_attr = "url" class GroupHookManager(CRUDMixin, RESTManager): _path = "/groups/%(group_id)s/hooks" _obj_cls = GroupHook _from_parent_attrs = {"group_id": "id"} _create_attrs = RequiredOptional( required=("url",), optional=( "push_events", "issues_events", "confidential_issues_events", "merge_requests_events", "tag_push_events", "note_events", "confidential_note_events", "job_events", "pipeline_events", "wiki_page_events", "deployment_events", "releases_events", "subgroup_events", "enable_ssl_verification", "token", ), ) _update_attrs = RequiredOptional( required=("url",), optional=( "push_events", "issues_events", "confidential_issues_events", "merge_requests_events", "tag_push_events", "note_events", "confidential_note_events", "job_events", "pipeline_events", "wiki_page_events", "deployment_events", "releases_events", "subgroup_events", "enable_ssl_verification", "token", ), ) python-gitlab-2.10.1/gitlab/v4/objects/issues.py000066400000000000000000000167261416141341200214760ustar00rootroot00000000000000from gitlab import cli from gitlab import exceptions as exc from gitlab import types from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import ( CreateMixin, CRUDMixin, DeleteMixin, ListMixin, ObjectDeleteMixin, ParticipantsMixin, RetrieveMixin, SaveMixin, SubscribableMixin, TimeTrackingMixin, TodoMixin, UserAgentDetailMixin, ) from .award_emojis import ProjectIssueAwardEmojiManager # noqa: F401 from .discussions import ProjectIssueDiscussionManager # noqa: F401 from .events import ( # noqa: F401 ProjectIssueResourceLabelEventManager, ProjectIssueResourceMilestoneEventManager, ProjectIssueResourceStateEventManager, ) from .notes import ProjectIssueNoteManager # noqa: F401 __all__ = [ "Issue", "IssueManager", "GroupIssue", "GroupIssueManager", "ProjectIssue", "ProjectIssueManager", "ProjectIssueLink", "ProjectIssueLinkManager", ] class Issue(RESTObject): _url = "/issues" _short_print_attr = "title" class IssueManager(RetrieveMixin, 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 = {"iids": types.ListAttribute, "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 = {"iids": types.ListAttribute, "labels": types.ListAttribute} 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"), ("resourcemilestoneevents", "ProjectIssueResourceMilestoneEventManager"), ("resourcestateevents", "ProjectIssueResourceStateEventManager"), ) @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 = RequiredOptional( required=("title",), optional=( "description", "confidential", "assignee_ids", "assignee_id", "milestone_id", "labels", "created_at", "due_date", "merge_request_to_resolve_discussions_of", "discussion_to_resolve", ), ) _update_attrs = RequiredOptional( optional=( "title", "description", "confidential", "assignee_ids", "assignee_id", "milestone_id", "labels", "state_event", "updated_at", "due_date", "discussion_locked", ), ) _types = {"iids": types.ListAttribute, "labels": types.ListAttribute} 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 = RequiredOptional(required=("target_project_id", "target_issue_iid")) @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 python-gitlab-2.10.1/gitlab/v4/objects/jobs.py000066400000000000000000000157261416141341200211170ustar00rootroot00000000000000from gitlab import cli from gitlab import exceptions as exc from gitlab import utils from gitlab.base import RESTManager, RESTObject from gitlab.mixins import RefreshMixin, RetrieveMixin __all__ = [ "ProjectJob", "ProjectJobManager", ] class ProjectJob(RefreshMixin, RESTObject): @cli.register_custom_action("ProjectJob") @exc.on_http_error(exc.GitlabJobCancelError) def cancel(self, **kwargs): """Cancel the job. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabJobCancelError: If the job could not be canceled """ path = "%s/%s/cancel" % (self.manager.path, self.get_id()) return 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()) return 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"} python-gitlab-2.10.1/gitlab/v4/objects/keys.py000066400000000000000000000011251416141341200211210ustar00rootroot00000000000000from gitlab.base import RESTManager, RESTObject from gitlab.mixins import GetMixin __all__ = [ "Key", "KeyManager", ] class Key(RESTObject): pass class KeyManager(GetMixin, RESTManager): _path = "/keys" _obj_cls = Key def get(self, id=None, **kwargs): if id is not None: return super(KeyManager, self).get(id, **kwargs) if "fingerprint" not in kwargs: raise AttributeError("Missing attribute: id or fingerprint") server_data = self.gitlab.http_get(self.path, **kwargs) return self._obj_cls(self, server_data) python-gitlab-2.10.1/gitlab/v4/objects/labels.py000066400000000000000000000113241416141341200214120ustar00rootroot00000000000000from gitlab import exceptions as exc from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import ( CreateMixin, DeleteMixin, ListMixin, ObjectDeleteMixin, RetrieveMixin, SaveMixin, SubscribableMixin, UpdateMixin, ) __all__ = [ "GroupLabel", "GroupLabelManager", "ProjectLabel", "ProjectLabelManager", ] 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 = RequiredOptional( required=("name", "color"), optional=("description", "priority") ) _update_attrs = RequiredOptional( required=("name",), optional=("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 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( RetrieveMixin, CreateMixin, UpdateMixin, DeleteMixin, RESTManager ): _path = "/projects/%(project_id)s/labels" _obj_cls = ProjectLabel _from_parent_attrs = {"project_id": "id"} _create_attrs = RequiredOptional( required=("name", "color"), optional=("description", "priority") ) _update_attrs = RequiredOptional( required=("name",), optional=("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) python-gitlab-2.10.1/gitlab/v4/objects/ldap.py000066400000000000000000000031361416141341200210720ustar00rootroot00000000000000from gitlab import exceptions as exc from gitlab.base import RESTManager, RESTObject, RESTObjectList __all__ = [ "LDAPGroup", "LDAPGroupManager", ] 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 RESTObjectList(self, self._obj_cls, obj) python-gitlab-2.10.1/gitlab/v4/objects/members.py000066400000000000000000000053251416141341200216060ustar00rootroot00000000000000from gitlab import types from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import ( CRUDMixin, DeleteMixin, ListMixin, MemberAllMixin, ObjectDeleteMixin, RetrieveMixin, SaveMixin, ) __all__ = [ "GroupBillableMember", "GroupBillableMemberManager", "GroupBillableMemberMembership", "GroupBillableMemberMembershipManager", "GroupMember", "GroupMemberManager", "GroupMemberAllManager", "ProjectMember", "ProjectMemberManager", "ProjectMemberAllManager", ] class GroupMember(SaveMixin, ObjectDeleteMixin, RESTObject): _short_print_attr = "username" class GroupMemberManager(MemberAllMixin, CRUDMixin, RESTManager): _path = "/groups/%(group_id)s/members" _obj_cls = GroupMember _from_parent_attrs = {"group_id": "id"} _create_attrs = RequiredOptional( required=("access_level", "user_id"), optional=("expires_at",) ) _update_attrs = RequiredOptional( required=("access_level",), optional=("expires_at",) ) _types = {"user_ids": types.ListAttribute} class GroupBillableMember(ObjectDeleteMixin, RESTObject): _short_print_attr = "username" _managers = (("memberships", "GroupBillableMemberMembershipManager"),) class GroupBillableMemberManager(ListMixin, DeleteMixin, RESTManager): _path = "/groups/%(group_id)s/billable_members" _obj_cls = GroupBillableMember _from_parent_attrs = {"group_id": "id"} _list_filters = ("search", "sort") class GroupBillableMemberMembership(RESTObject): _id_attr = "user_id" class GroupBillableMemberMembershipManager(ListMixin, RESTManager): _path = "/groups/%(group_id)s/billable_members/%(user_id)s/memberships" _obj_cls = GroupBillableMemberMembership _from_parent_attrs = {"group_id": "group_id", "user_id": "id"} class GroupMemberAllManager(RetrieveMixin, RESTManager): _path = "/groups/%(group_id)s/members/all" _obj_cls = GroupMember _from_parent_attrs = {"group_id": "id"} class ProjectMember(SaveMixin, ObjectDeleteMixin, RESTObject): _short_print_attr = "username" class ProjectMemberManager(MemberAllMixin, CRUDMixin, RESTManager): _path = "/projects/%(project_id)s/members" _obj_cls = ProjectMember _from_parent_attrs = {"project_id": "id"} _create_attrs = RequiredOptional( required=("access_level", "user_id"), optional=("expires_at",) ) _update_attrs = RequiredOptional( required=("access_level",), optional=("expires_at",) ) _types = {"user_ids": types.ListAttribute} class ProjectMemberAllManager(RetrieveMixin, RESTManager): _path = "/projects/%(project_id)s/members/all" _obj_cls = ProjectMember _from_parent_attrs = {"project_id": "id"} python-gitlab-2.10.1/gitlab/v4/objects/merge_request_approvals.py000066400000000000000000000166061416141341200251160ustar00rootroot00000000000000from gitlab import exceptions as exc from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import ( CreateMixin, DeleteMixin, GetWithoutIdMixin, ListMixin, ObjectDeleteMixin, SaveMixin, UpdateMixin, ) __all__ = [ "ProjectApproval", "ProjectApprovalManager", "ProjectApprovalRule", "ProjectApprovalRuleManager", "ProjectMergeRequestApproval", "ProjectMergeRequestApprovalManager", "ProjectMergeRequestApprovalRule", "ProjectMergeRequestApprovalRuleManager", ] 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 = RequiredOptional( optional=( "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 = RequiredOptional( required=("name", "approvals_required"), optional=("user_ids", "group_ids", "protected_branch_ids"), ) 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 = RequiredOptional(required=("approvals_required",)) _update_uses_post = True @exc.on_http_error(exc.GitlabUpdateError) def set_approvers( self, approvals_required, approver_ids=None, approver_group_ids=None, approval_rule_name="name", **kwargs ): """Change MR-level allowed approvers and approver groups. Args: approvals_required (integer): The number of required approvals for this rule approver_ids (list of integers): 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 [] data = { "name": approval_rule_name, "approvals_required": approvals_required, "rule_type": "regular", "user_ids": approver_ids, "group_ids": approver_group_ids, } approval_rules = self._parent.approval_rules """ update any existing approval rule matching the name""" existing_approval_rules = approval_rules.list() for ar in existing_approval_rules: if ar.name == approval_rule_name: ar.user_ids = data["user_ids"] ar.approvals_required = data["approvals_required"] ar.group_ids = data["group_ids"] ar.save() return ar """ if there was no rule matching the rule name, create a new one""" return approval_rules.create(data=data) class ProjectMergeRequestApprovalRule(SaveMixin, RESTObject): _id_attr = "approval_rule_id" _short_print_attr = "approval_rule" @exc.on_http_error(exc.GitlabUpdateError) 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 """ # There is a mismatch between the name of our id attribute and the put REST API name for the # project_id, so we override it here. self.approval_rule_id = self.id self.merge_request_iid = self._parent_attrs["mr_iid"] self.id = self._parent_attrs["project_id"] # save will update self.id with the result from the server, so no need to overwrite with # what it was before we overwrote it.""" SaveMixin.save(self, **kwargs) class ProjectMergeRequestApprovalRuleManager( ListMixin, UpdateMixin, CreateMixin, RESTManager ): _path = "/projects/%(project_id)s/merge_requests/%(mr_iid)s/approval_rules" _obj_cls = ProjectMergeRequestApprovalRule _from_parent_attrs = {"project_id": "project_id", "mr_iid": "iid"} _list_filters = ("name", "rule_type") _update_attrs = RequiredOptional( required=( "id", "merge_request_iid", "approval_rule_id", "name", "approvals_required", ), optional=("user_ids", "group_ids"), ) # Important: When approval_project_rule_id is set, the name, users and groups of # project-level rule will be copied. The approvals_required specified will be used. """ _create_attrs = RequiredOptional( required=("id", "merge_request_iid", "name", "approvals_required"), optional=("approval_project_rule_id", "user_ids", "group_ids"), ) 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 """ new_data = data.copy() new_data["id"] = self._from_parent_attrs["project_id"] new_data["merge_request_iid"] = self._from_parent_attrs["mr_iid"] return CreateMixin.create(self, new_data, **kwargs) python-gitlab-2.10.1/gitlab/v4/objects/merge_requests.py000066400000000000000000000337771416141341200232220ustar00rootroot00000000000000from gitlab import cli from gitlab import exceptions as exc from gitlab import types from gitlab.base import RequiredOptional, RESTManager, RESTObject, RESTObjectList from gitlab.mixins import ( CRUDMixin, ListMixin, ObjectDeleteMixin, ParticipantsMixin, RetrieveMixin, SaveMixin, SubscribableMixin, TimeTrackingMixin, TodoMixin, ) from .award_emojis import ProjectMergeRequestAwardEmojiManager # noqa: F401 from .commits import ProjectCommit, ProjectCommitManager from .discussions import ProjectMergeRequestDiscussionManager # noqa: F401 from .events import ( # noqa: F401 ProjectMergeRequestResourceLabelEventManager, ProjectMergeRequestResourceMilestoneEventManager, ProjectMergeRequestResourceStateEventManager, ) from .issues import ProjectIssue, ProjectIssueManager from .merge_request_approvals import ( # noqa: F401 ProjectMergeRequestApprovalManager, ProjectMergeRequestApprovalRuleManager, ) from .notes import ProjectMergeRequestNoteManager # noqa: F401 from .pipelines import ProjectMergeRequestPipelineManager # noqa: F401 __all__ = [ "MergeRequest", "MergeRequestManager", "GroupMergeRequest", "GroupMergeRequestManager", "ProjectMergeRequest", "ProjectMergeRequestManager", "ProjectDeploymentMergeRequest", "ProjectDeploymentMergeRequestManager", "ProjectMergeRequestDiff", "ProjectMergeRequestDiffManager", ] class MergeRequest(RESTObject): pass class MergeRequestManager(ListMixin, RESTManager): _path = "/merge_requests" _obj_cls = MergeRequest _list_filters = ( "state", "order_by", "sort", "milestone", "view", "labels", "with_labels_details", "with_merge_status_recheck", "created_after", "created_before", "updated_after", "updated_before", "scope", "author_id", "author_username", "assignee_id", "approver_ids", "approved_by_ids", "reviewer_id", "reviewer_username", "my_reaction_emoji", "source_branch", "target_branch", "search", "in", "wip", "not", "environment", "deployed_before", "deployed_after", ) _types = { "approver_ids": types.ListAttribute, "approved_by_ids": types.ListAttribute, "in": types.ListAttribute, "labels": types.ListAttribute, } 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", "approver_ids", "approved_by_ids", "my_reaction_emoji", "source_branch", "target_branch", "search", "wip", ) _types = { "approver_ids": types.ListAttribute, "approved_by_ids": types.ListAttribute, "labels": types.ListAttribute, } class ProjectMergeRequest( SubscribableMixin, TodoMixin, TimeTrackingMixin, ParticipantsMixin, SaveMixin, ObjectDeleteMixin, RESTObject, ): _id_attr = "iid" _managers = ( ("approvals", "ProjectMergeRequestApprovalManager"), ("approval_rules", "ProjectMergeRequestApprovalRuleManager"), ("awardemojis", "ProjectMergeRequestAwardEmojiManager"), ("diffs", "ProjectMergeRequestDiffManager"), ("discussions", "ProjectMergeRequestDiscussionManager"), ("notes", "ProjectMergeRequestNoteManager"), ("pipelines", "ProjectMergeRequestPipelineManager"), ("resourcelabelevents", "ProjectMergeRequestResourceLabelEventManager"), ("resourcemilestoneevents", "ProjectMergeRequestResourceMilestoneEventManager"), ("resourcestateevents", "ProjectMergeRequestResourceStateEventManager"), ) @cli.register_custom_action("ProjectMergeRequest") @exc.on_http_error(exc.GitlabMROnBuildSuccessError) def cancel_merge_when_pipeline_succeeds(self, **kwargs): """Cancel merge when the pipeline succeeds. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabMROnBuildSuccessError: If the server could not handle the request """ path = "%s/%s/cancel_merge_when_pipeline_succeeds" % ( self.manager.path, self.get_id(), ) server_data = self.manager.gitlab.http_put(path, **kwargs) self._update_attrs(server_data) @cli.register_custom_action("ProjectMergeRequest") @exc.on_http_error(exc.GitlabListError) def closes_issues(self, **kwargs): """List issues that will close on merge." Args: all (bool): If True, return all the items, without pagination per_page (int): Number of items to retrieve per request page (int): ID of the page to return (starts with page 1) as_list (bool): If set to False and no pagination option is defined, return a generator instead of a list **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabListError: If the list could not be retrieved Returns: RESTObjectList: List of issues """ path = "%s/%s/closes_issues" % (self.manager.path, self.get_id()) data_list = self.manager.gitlab.http_list(path, as_list=False, **kwargs) manager = ProjectIssueManager(self.manager.gitlab, parent=self.manager._parent) return RESTObjectList(manager, ProjectIssue, data_list) @cli.register_custom_action("ProjectMergeRequest") @exc.on_http_error(exc.GitlabListError) def commits(self, **kwargs): """List the merge request commits. Args: all (bool): If True, return all the items, without pagination per_page (int): Number of items to retrieve per request page (int): ID of the page to return (starts with page 1) as_list (bool): If set to False and no pagination option is defined, return a generator instead of a list **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabListError: If the list could not be retrieved Returns: RESTObjectList: The list of commits """ path = "%s/%s/commits" % (self.manager.path, self.get_id()) data_list = self.manager.gitlab.http_list(path, as_list=False, **kwargs) manager = ProjectCommitManager(self.manager.gitlab, parent=self.manager._parent) return RESTObjectList(manager, ProjectCommit, data_list) @cli.register_custom_action("ProjectMergeRequest") @exc.on_http_error(exc.GitlabListError) def changes(self, **kwargs): """List the merge request changes. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabListError: If the list could not be retrieved Returns: RESTObjectList: List of changes """ path = "%s/%s/changes" % (self.manager.path, self.get_id()) return self.manager.gitlab.http_get(path, **kwargs) @cli.register_custom_action("ProjectMergeRequest", tuple(), ("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") @exc.on_http_error(exc.GitlabGetError) def merge_ref(self, **kwargs): """Attempt to merge changes between source and target branches into `refs/merge-requests/:iid/merge`. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabGetError: If cannot be merged """ path = "%s/%s/merge_ref" % (self.manager.path, self.get_id()) return self.manager.gitlab.http_get(path, **kwargs) @cli.register_custom_action( "ProjectMergeRequest", tuple(), ( "merge_commit_message", "should_remove_source_branch", "merge_when_pipeline_succeeds", ), ) @exc.on_http_error(exc.GitlabMRClosedError) def merge( self, merge_commit_message=None, should_remove_source_branch=False, merge_when_pipeline_succeeds=False, **kwargs ): """Accept the merge request. Args: merge_commit_message (bool): Commit message should_remove_source_branch (bool): If True, removes the source branch merge_when_pipeline_succeeds (bool): Wait for the build to succeed, then merge **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabMRClosedError: If the merge failed """ path = "%s/%s/merge" % (self.manager.path, self.get_id()) data = {} if merge_commit_message: data["merge_commit_message"] = merge_commit_message if should_remove_source_branch is not None: data["should_remove_source_branch"] = should_remove_source_branch 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 = RequiredOptional( required=("source_branch", "target_branch", "title"), optional=( "assignee_id", "description", "target_project_id", "labels", "milestone_id", "remove_source_branch", "allow_maintainer_to_push", "squash", "reviewer_ids", ), ) _update_attrs = RequiredOptional( optional=( "target_branch", "assignee_id", "title", "description", "state_event", "labels", "milestone_id", "remove_source_branch", "discussion_locked", "allow_maintainer_to_push", "squash", "reviewer_ids", ), ) _list_filters = ( "state", "order_by", "sort", "milestone", "view", "labels", "created_after", "created_before", "updated_after", "updated_before", "scope", "iids", "author_id", "assignee_id", "approver_ids", "approved_by_ids", "my_reaction_emoji", "source_branch", "target_branch", "search", "wip", ) _types = { "approver_ids": types.ListAttribute, "approved_by_ids": types.ListAttribute, "iids": types.ListAttribute, "labels": types.ListAttribute, } class ProjectDeploymentMergeRequest(MergeRequest): pass class ProjectDeploymentMergeRequestManager(MergeRequestManager): _path = "/projects/%(project_id)s/deployments/%(deployment_id)s/merge_requests" _obj_cls = ProjectDeploymentMergeRequest _from_parent_attrs = {"deployment_id": "id", "project_id": "project_id"} 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"} python-gitlab-2.10.1/gitlab/v4/objects/milestones.py000066400000000000000000000151511416141341200223340ustar00rootroot00000000000000from gitlab import cli from gitlab import exceptions as exc from gitlab import types from gitlab.base import RequiredOptional, RESTManager, RESTObject, RESTObjectList from gitlab.mixins import CRUDMixin, ObjectDeleteMixin, SaveMixin from .issues import GroupIssue, GroupIssueManager, ProjectIssue, ProjectIssueManager from .merge_requests import ( GroupMergeRequest, ProjectMergeRequest, ProjectMergeRequestManager, ) __all__ = [ "GroupMilestone", "GroupMilestoneManager", "ProjectMilestone", "ProjectMilestoneManager", ] 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 = RequiredOptional( required=("title",), optional=("description", "due_date", "start_date") ) _update_attrs = RequiredOptional( optional=("title", "description", "due_date", "start_date", "state_event"), ) _list_filters = ("iids", "state", "search") _types = {"iids": 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 = RequiredOptional( required=("title",), optional=("description", "due_date", "start_date", "state_event"), ) _update_attrs = RequiredOptional( optional=("title", "description", "due_date", "start_date", "state_event"), ) _list_filters = ("iids", "state", "search") _types = {"iids": types.ListAttribute} python-gitlab-2.10.1/gitlab/v4/objects/namespaces.py000066400000000000000000000005011416141341200222620ustar00rootroot00000000000000from gitlab.base import RESTManager, RESTObject from gitlab.mixins import RetrieveMixin __all__ = [ "Namespace", "NamespaceManager", ] class Namespace(RESTObject): pass class NamespaceManager(RetrieveMixin, RESTManager): _path = "/namespaces" _obj_cls = Namespace _list_filters = ("search",) python-gitlab-2.10.1/gitlab/v4/objects/notes.py000066400000000000000000000123771416141341200213110ustar00rootroot00000000000000from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import ( CreateMixin, CRUDMixin, DeleteMixin, GetMixin, ObjectDeleteMixin, RetrieveMixin, SaveMixin, UpdateMixin, ) from .award_emojis import ( # noqa: F401 ProjectIssueNoteAwardEmojiManager, ProjectMergeRequestNoteAwardEmojiManager, ProjectSnippetNoteAwardEmojiManager, ) __all__ = [ "ProjectNote", "ProjectNoteManager", "ProjectCommitDiscussionNote", "ProjectCommitDiscussionNoteManager", "ProjectIssueNote", "ProjectIssueNoteManager", "ProjectIssueDiscussionNote", "ProjectIssueDiscussionNoteManager", "ProjectMergeRequestNote", "ProjectMergeRequestNoteManager", "ProjectMergeRequestDiscussionNote", "ProjectMergeRequestDiscussionNoteManager", "ProjectSnippetNote", "ProjectSnippetNoteManager", "ProjectSnippetDiscussionNote", "ProjectSnippetDiscussionNoteManager", ] 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 = RequiredOptional(required=("body",)) 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 = RequiredOptional( required=("body",), optional=("created_at", "position") ) _update_attrs = RequiredOptional(required=("body",)) 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 = RequiredOptional(required=("body",), optional=("created_at",)) _update_attrs = RequiredOptional(required=("body",)) 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 = RequiredOptional(required=("body",), optional=("created_at",)) _update_attrs = RequiredOptional(required=("body",)) 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 = RequiredOptional(required=("body",)) _update_attrs = RequiredOptional(required=("body",)) 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 = RequiredOptional(required=("body",), optional=("created_at",)) _update_attrs = RequiredOptional(required=("body",)) 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 = RequiredOptional(required=("body",)) _update_attrs = RequiredOptional(required=("body",)) 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 = RequiredOptional(required=("body",), optional=("created_at",)) _update_attrs = RequiredOptional(required=("body",)) python-gitlab-2.10.1/gitlab/v4/objects/notification_settings.py000066400000000000000000000031011416141341200245500ustar00rootroot00000000000000from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import GetWithoutIdMixin, SaveMixin, UpdateMixin __all__ = [ "NotificationSettings", "NotificationSettingsManager", "GroupNotificationSettings", "GroupNotificationSettingsManager", "ProjectNotificationSettings", "ProjectNotificationSettingsManager", ] class NotificationSettings(SaveMixin, RESTObject): _id_attr = None class NotificationSettingsManager(GetWithoutIdMixin, UpdateMixin, RESTManager): _path = "/notification_settings" _obj_cls = NotificationSettings _update_attrs = RequiredOptional( optional=( "level", "notification_email", "new_note", "new_issue", "reopen_issue", "close_issue", "reassign_issue", "new_merge_request", "reopen_merge_request", "close_merge_request", "reassign_merge_request", "merge_merge_request", ), ) class GroupNotificationSettings(NotificationSettings): pass class GroupNotificationSettingsManager(NotificationSettingsManager): _path = "/groups/%(group_id)s/notification_settings" _obj_cls = GroupNotificationSettings _from_parent_attrs = {"group_id": "id"} class ProjectNotificationSettings(NotificationSettings): pass class ProjectNotificationSettingsManager(NotificationSettingsManager): _path = "/projects/%(project_id)s/notification_settings" _obj_cls = ProjectNotificationSettings _from_parent_attrs = {"project_id": "id"} python-gitlab-2.10.1/gitlab/v4/objects/packages.py000066400000000000000000000123321416141341200217260ustar00rootroot00000000000000from pathlib import Path from typing import Any, Callable, Optional, TYPE_CHECKING, Union import requests from gitlab import cli from gitlab import exceptions as exc from gitlab import utils from gitlab.base import RESTManager, RESTObject from gitlab.mixins import DeleteMixin, GetMixin, ListMixin, ObjectDeleteMixin __all__ = [ "GenericPackage", "GenericPackageManager", "GroupPackage", "GroupPackageManager", "ProjectPackage", "ProjectPackageManager", "ProjectPackageFile", "ProjectPackageFileManager", ] class GenericPackage(RESTObject): _id_attr = "package_name" class GenericPackageManager(RESTManager): _path = "/projects/%(project_id)s/packages/generic" _obj_cls = GenericPackage _from_parent_attrs = {"project_id": "id"} @cli.register_custom_action( "GenericPackageManager", ("package_name", "package_version", "file_name", "path"), ) @exc.on_http_error(exc.GitlabUploadError) def upload( self, package_name: str, package_version: str, file_name: str, path: Union[str, Path], **kwargs, ) -> GenericPackage: """Upload a file as a generic package. Args: package_name (str): The package name. Must follow generic package name regex rules package_version (str): The package version. Must follow semantic version regex rules file_name (str): The name of the file as uploaded in the registry path (str): The path to a local file to upload Raises: GitlabConnectionError: If the server cannot be reached GitlabUploadError: If the file upload fails GitlabUploadError: If ``filepath`` cannot be read Returns: GenericPackage: An object storing the metadata of the uploaded package. """ try: with open(path, "rb") as f: file_data = f.read() except OSError: raise exc.GitlabUploadError(f"Failed to read package file {path}") url = f"{self._computed_path}/{package_name}/{package_version}/{file_name}" server_data = self.gitlab.http_put(url, post_data=file_data, raw=True, **kwargs) return self._obj_cls( self, attrs={ "package_name": package_name, "package_version": package_version, "file_name": file_name, "path": path, "message": server_data["message"], }, ) @cli.register_custom_action( "GenericPackageManager", ("package_name", "package_version", "file_name"), ) @exc.on_http_error(exc.GitlabGetError) def download( self, package_name: str, package_version: str, file_name: str, streamed: bool = False, action: Optional[Callable] = None, chunk_size: int = 1024, **kwargs: Any, ) -> Optional[bytes]: """Download a generic package. Args: package_name (str): The package name. package_version (str): The package version. file_name (str): The name of the file in the registry 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 package content if streamed is False, None otherwise """ path = f"{self._computed_path}/{package_name}/{package_version}/{file_name}" result = self.gitlab.http_get(path, streamed=streamed, raw=True, **kwargs) if TYPE_CHECKING: assert isinstance(result, requests.Response) return utils.response_content(result, streamed, action, chunk_size) class GroupPackage(RESTObject): pass class GroupPackageManager(ListMixin, RESTManager): _path = "/groups/%(group_id)s/packages" _obj_cls = GroupPackage _from_parent_attrs = {"group_id": "id"} _list_filters = ( "exclude_subgroups", "order_by", "sort", "package_type", "package_name", ) class ProjectPackage(ObjectDeleteMixin, RESTObject): _managers = (("package_files", "ProjectPackageFileManager"),) class ProjectPackageManager(ListMixin, GetMixin, DeleteMixin, RESTManager): _path = "/projects/%(project_id)s/packages" _obj_cls = ProjectPackage _from_parent_attrs = {"project_id": "id"} _list_filters = ( "order_by", "sort", "package_type", "package_name", ) class ProjectPackageFile(RESTObject): pass class ProjectPackageFileManager(ListMixin, RESTManager): _path = "/projects/%(project_id)s/packages/%(package_id)s/package_files" _obj_cls = ProjectPackageFile _from_parent_attrs = {"project_id": "project_id", "package_id": "id"} python-gitlab-2.10.1/gitlab/v4/objects/pages.py000066400000000000000000000015711416141341200212520ustar00rootroot00000000000000from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import CRUDMixin, ListMixin, ObjectDeleteMixin, SaveMixin __all__ = [ "PagesDomain", "PagesDomainManager", "ProjectPagesDomain", "ProjectPagesDomainManager", ] class PagesDomain(RESTObject): _id_attr = "domain" class PagesDomainManager(ListMixin, RESTManager): _path = "/pages/domains" _obj_cls = PagesDomain 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 = RequiredOptional( required=("domain",), optional=("certificate", "key") ) _update_attrs = RequiredOptional(optional=("certificate", "key")) python-gitlab-2.10.1/gitlab/v4/objects/personal_access_tokens.py000066400000000000000000000005701416141341200247000ustar00rootroot00000000000000from gitlab.base import RESTManager, RESTObject from gitlab.mixins import ListMixin __all__ = [ "PersonalAccessToken", "PersonalAccessTokenManager", ] class PersonalAccessToken(RESTObject): pass class PersonalAccessTokenManager(ListMixin, RESTManager): _path = "/personal_access_tokens" _obj_cls = PersonalAccessToken _list_filters = ("user_id",) python-gitlab-2.10.1/gitlab/v4/objects/pipelines.py000066400000000000000000000207161416141341200221450ustar00rootroot00000000000000import warnings from gitlab import cli from gitlab import exceptions as exc from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import ( CreateMixin, CRUDMixin, DeleteMixin, GetWithoutIdMixin, ListMixin, ObjectDeleteMixin, RefreshMixin, RetrieveMixin, SaveMixin, UpdateMixin, ) __all__ = [ "ProjectMergeRequestPipeline", "ProjectMergeRequestPipelineManager", "ProjectPipeline", "ProjectPipelineManager", "ProjectPipelineJob", "ProjectPipelineJobManager", "ProjectPipelineBridge", "ProjectPipelineBridgeManager", "ProjectPipelineVariable", "ProjectPipelineVariableManager", "ProjectPipelineScheduleVariable", "ProjectPipelineScheduleVariableManager", "ProjectPipelineSchedule", "ProjectPipelineScheduleManager", "ProjectPipelineTestReport", "ProjectPipelineTestReportManager", ] class ProjectMergeRequestPipeline(RESTObject): pass class ProjectMergeRequestPipelineManager(CreateMixin, ListMixin, RESTManager): _path = "/projects/%(project_id)s/merge_requests/%(mr_iid)s/pipelines" _obj_cls = ProjectMergeRequestPipeline _from_parent_attrs = {"project_id": "project_id", "mr_iid": "iid"} # If the manager was called directly as a callable via # mr.pipelines(), execute the deprecated method for now. # TODO: in python-gitlab 3.0.0, remove this method entirely. @cli.register_custom_action("ProjectMergeRequest", custom_action="pipelines") @exc.on_http_error(exc.GitlabListError) def __call__(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 """ warnings.warn( "Calling the ProjectMergeRequest.pipelines() method on " "merge request objects directly is deprecated and will be replaced " "by ProjectMergeRequest.pipelines.list() in python-gitlab 3.0.0.\n", DeprecationWarning, ) return self.list(**kwargs) class ProjectPipeline(RefreshMixin, ObjectDeleteMixin, RESTObject): _managers = ( ("jobs", "ProjectPipelineJobManager"), ("bridges", "ProjectPipelineBridgeManager"), ("variables", "ProjectPipelineVariableManager"), ("test_report", "ProjectPipelineTestReportManager"), ) @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()) return 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()) return 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 = RequiredOptional(required=("ref",)) 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 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", "include_retried") class ProjectPipelineBridge(RESTObject): pass class ProjectPipelineBridgeManager(ListMixin, RESTManager): _path = "/projects/%(project_id)s/pipelines/%(pipeline_id)s/bridges" _obj_cls = ProjectPipelineBridge _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 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 = RequiredOptional(required=("key", "value")) _update_attrs = RequiredOptional(required=("key", "value")) 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) @cli.register_custom_action("ProjectPipelineSchedule") @exc.on_http_error(exc.GitlabPipelinePlayError) def play(self, **kwargs): """Trigger a new scheduled pipeline, which runs immediately. The next scheduled run of this pipeline is not affected. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabPipelinePlayError: If the request failed """ path = "%s/%s/play" % (self.manager.path, self.get_id()) server_data = self.manager.gitlab.http_post(path, **kwargs) self._update_attrs(server_data) return server_data class ProjectPipelineScheduleManager(CRUDMixin, RESTManager): _path = "/projects/%(project_id)s/pipeline_schedules" _obj_cls = ProjectPipelineSchedule _from_parent_attrs = {"project_id": "id"} _create_attrs = RequiredOptional( required=("description", "ref", "cron"), optional=("cron_timezone", "active") ) _update_attrs = RequiredOptional( optional=("description", "ref", "cron", "cron_timezone", "active"), ) class ProjectPipelineTestReport(RESTObject): _id_attr = None class ProjectPipelineTestReportManager(GetWithoutIdMixin, RESTManager): _path = "/projects/%(project_id)s/pipelines/%(pipeline_id)s/test_report" _obj_cls = ProjectPipelineTestReport _from_parent_attrs = {"project_id": "project_id", "pipeline_id": "id"} python-gitlab-2.10.1/gitlab/v4/objects/project_access_tokens.py000066400000000000000000000007511416141341200245240ustar00rootroot00000000000000from gitlab.base import RESTManager, RESTObject from gitlab.mixins import CreateMixin, DeleteMixin, ListMixin, ObjectDeleteMixin __all__ = [ "ProjectAccessToken", "ProjectAccessTokenManager", ] class ProjectAccessToken(ObjectDeleteMixin, RESTObject): pass class ProjectAccessTokenManager(ListMixin, CreateMixin, DeleteMixin, RESTManager): _path = "/projects/%(project_id)s/access_tokens" _obj_cls = ProjectAccessToken _from_parent_attrs = {"project_id": "id"} python-gitlab-2.10.1/gitlab/v4/objects/projects.py000066400000000000000000001166131416141341200220100ustar00rootroot00000000000000from typing import Any, Callable, cast, Dict, List, Optional, TYPE_CHECKING, Union import requests from gitlab import cli, client from gitlab import exceptions as exc from gitlab import types, utils from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import ( CreateMixin, CRUDMixin, ListMixin, ObjectDeleteMixin, RefreshMixin, SaveMixin, UpdateMixin, ) from .access_requests import ProjectAccessRequestManager # noqa: F401 from .audit_events import ProjectAuditEventManager # noqa: F401 from .badges import ProjectBadgeManager # noqa: F401 from .boards import ProjectBoardManager # noqa: F401 from .branches import ProjectBranchManager, ProjectProtectedBranchManager # noqa: F401 from .clusters import ProjectClusterManager # noqa: F401 from .commits import ProjectCommitManager # noqa: F401 from .container_registry import ProjectRegistryRepositoryManager # noqa: F401 from .custom_attributes import ProjectCustomAttributeManager # noqa: F401 from .deploy_keys import ProjectKeyManager # noqa: F401 from .deploy_tokens import ProjectDeployTokenManager # noqa: F401 from .deployments import ProjectDeploymentManager # noqa: F401 from .environments import ProjectEnvironmentManager # noqa: F401 from .events import ProjectEventManager # noqa: F401 from .export_import import ProjectExportManager, ProjectImportManager # noqa: F401 from .files import ProjectFileManager # noqa: F401 from .hooks import ProjectHookManager # noqa: F401 from .issues import ProjectIssueManager # noqa: F401 from .jobs import ProjectJobManager # noqa: F401 from .labels import ProjectLabelManager # noqa: F401 from .members import ProjectMemberAllManager, ProjectMemberManager # noqa: F401 from .merge_request_approvals import ( # noqa: F401 ProjectApprovalManager, ProjectApprovalRuleManager, ) from .merge_requests import ProjectMergeRequestManager # noqa: F401 from .milestones import ProjectMilestoneManager # noqa: F401 from .notes import ProjectNoteManager # noqa: F401 from .notification_settings import ProjectNotificationSettingsManager # noqa: F401 from .packages import GenericPackageManager, ProjectPackageManager # noqa: F401 from .pages import ProjectPagesDomainManager # noqa: F401 from .pipelines import ( # noqa: F401 ProjectPipeline, ProjectPipelineManager, ProjectPipelineScheduleManager, ) from .project_access_tokens import ProjectAccessTokenManager # noqa: F401 from .push_rules import ProjectPushRulesManager # noqa: F401 from .releases import ProjectReleaseManager # noqa: F401 from .repositories import RepositoryMixin from .runners import ProjectRunnerManager # noqa: F401 from .services import ProjectServiceManager # noqa: F401 from .snippets import ProjectSnippetManager # noqa: F401 from .statistics import ( # noqa: F401 ProjectAdditionalStatisticsManager, ProjectIssuesStatisticsManager, ) from .tags import ProjectProtectedTagManager, ProjectTagManager # noqa: F401 from .triggers import ProjectTriggerManager # noqa: F401 from .users import ProjectUserManager # noqa: F401 from .variables import ProjectVariableManager # noqa: F401 from .wikis import ProjectWikiManager # noqa: F401 __all__ = [ "GroupProject", "GroupProjectManager", "Project", "ProjectManager", "ProjectFork", "ProjectForkManager", "ProjectRemoteMirror", "ProjectRemoteMirrorManager", ] 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 Project(RefreshMixin, SaveMixin, ObjectDeleteMixin, RepositoryMixin, RESTObject): _short_print_attr = "path" _managers = ( ("access_tokens", "ProjectAccessTokenManager"), ("accessrequests", "ProjectAccessRequestManager"), ("approvals", "ProjectApprovalManager"), ("approvalrules", "ProjectApprovalRuleManager"), ("badges", "ProjectBadgeManager"), ("boards", "ProjectBoardManager"), ("branches", "ProjectBranchManager"), ("jobs", "ProjectJobManager"), ("commits", "ProjectCommitManager"), ("customattributes", "ProjectCustomAttributeManager"), ("deployments", "ProjectDeploymentManager"), ("environments", "ProjectEnvironmentManager"), ("events", "ProjectEventManager"), ("audit_events", "ProjectAuditEventManager"), ("exports", "ProjectExportManager"), ("files", "ProjectFileManager"), ("forks", "ProjectForkManager"), ("generic_packages", "GenericPackageManager"), ("hooks", "ProjectHookManager"), ("keys", "ProjectKeyManager"), ("imports", "ProjectImportManager"), ("issues", "ProjectIssueManager"), ("labels", "ProjectLabelManager"), ("members", "ProjectMemberManager"), ("members_all", "ProjectMemberAllManager"), ("mergerequests", "ProjectMergeRequestManager"), ("milestones", "ProjectMilestoneManager"), ("notes", "ProjectNoteManager"), ("notificationsettings", "ProjectNotificationSettingsManager"), ("packages", "ProjectPackageManager"), ("pagesdomains", "ProjectPagesDomainManager"), ("pipelines", "ProjectPipelineManager"), ("protectedbranches", "ProjectProtectedBranchManager"), ("protectedtags", "ProjectProtectedTagManager"), ("pipelineschedules", "ProjectPipelineScheduleManager"), ("pushrules", "ProjectPushRulesManager"), ("releases", "ProjectReleaseManager"), ("remote_mirrors", "ProjectRemoteMirrorManager"), ("repositories", "ProjectRegistryRepositoryManager"), ("runners", "ProjectRunnerManager"), ("services", "ProjectServiceManager"), ("snippets", "ProjectSnippetManager"), ("tags", "ProjectTagManager"), ("users", "ProjectUserManager"), ("triggers", "ProjectTriggerManager"), ("variables", "ProjectVariableManager"), ("wikis", "ProjectWikiManager"), ("clusters", "ProjectClusterManager"), ("additionalstatistics", "ProjectAdditionalStatisticsManager"), ("issues_statistics", "ProjectIssuesStatisticsManager"), ("issuesstatistics", "ProjectIssuesStatisticsManager"), # Deprecated ("deploytokens", "ProjectDeployTokenManager"), ) @cli.register_custom_action("Project", ("forked_from_id",)) @exc.on_http_error(exc.GitlabCreateError) def create_fork_relation(self, forked_from_id: int, **kwargs: Any) -> None: """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: Any) -> None: """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.GitlabGetError) def languages(self, **kwargs: Any) -> Union[Dict[str, Any], requests.Response]: """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: Any) -> None: """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) if TYPE_CHECKING: assert isinstance(server_data, dict) self._update_attrs(server_data) @cli.register_custom_action("Project") @exc.on_http_error(exc.GitlabDeleteError) def unstar(self, **kwargs: Any) -> None: """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) if TYPE_CHECKING: assert isinstance(server_data, dict) self._update_attrs(server_data) @cli.register_custom_action("Project") @exc.on_http_error(exc.GitlabCreateError) def archive(self, **kwargs: Any) -> None: """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) if TYPE_CHECKING: assert isinstance(server_data, dict) self._update_attrs(server_data) @cli.register_custom_action("Project") @exc.on_http_error(exc.GitlabDeleteError) def unarchive(self, **kwargs: Any) -> None: """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) if TYPE_CHECKING: assert isinstance(server_data, dict) 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: int, group_access: int, expires_at: Optional[str] = None, **kwargs: Any ) -> None: """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: int, **kwargs: Any) -> None: """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: str, token: str, variables: Optional[Dict[str, Any]] = None, **kwargs: Any ) -> ProjectPipeline: """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) if TYPE_CHECKING: assert isinstance(attrs, dict) return ProjectPipeline(self.pipelines, attrs) @cli.register_custom_action("Project") @exc.on_http_error(exc.GitlabHousekeepingError) def housekeeping(self, **kwargs: Any) -> None: """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: str, filedata: Optional[bytes] = None, filepath: Optional[str] = None, **kwargs: Any ) -> Dict[str, Any]: """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 exc.GitlabUploadError("No file contents or path specified") if filedata is not None and filepath is not None: raise exc.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) if TYPE_CHECKING: assert isinstance(data, dict) 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: bool = False, streamed: bool = False, action: Optional[Callable] = None, chunk_size: int = 1024, **kwargs: Any ) -> Optional[bytes]: """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 ) if TYPE_CHECKING: assert isinstance(result, requests.Response) 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: str, search: str, **kwargs: Any ) -> Union[client.GitlabList, List[Dict[str, Any]]]: """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: Any) -> None: """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: str, **kwargs: Any) -> None: """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", "job"), ("job_token",)) @exc.on_http_error(exc.GitlabGetError) def artifacts( self, ref_name: str, job: str, streamed: bool = False, action: Optional[Callable] = None, chunk_size: int = 1024, **kwargs: Any ) -> Optional[bytes]: """Get the job artifacts archive from a specific tag or branch. 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. job_token (str): Job token for multi-project pipeline triggers. 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/download" % (self.get_id(), ref_name) result = self.manager.gitlab.http_get( path, job=job, streamed=streamed, raw=True, **kwargs ) if TYPE_CHECKING: assert isinstance(result, requests.Response) return utils.response_content(result, streamed, action, chunk_size) @cli.register_custom_action("Project", ("ref_name", "artifact_path", "job")) @exc.on_http_error(exc.GitlabGetError) def artifact( self, ref_name: str, artifact_path: str, job: str, streamed: bool = False, action: Optional[Callable] = None, chunk_size: int = 1024, **kwargs: Any ) -> Optional[bytes]: """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 ) if TYPE_CHECKING: assert isinstance(result, requests.Response) return utils.response_content(result, streamed, action, chunk_size) class ProjectManager(CRUDMixin, RESTManager): _path = "/projects" _obj_cls = Project # Please keep these _create_attrs in same order as they are at: # https://docs.gitlab.com/ee/api/projects.html#create-project _create_attrs = RequiredOptional( optional=( "name", "path", "allow_merge_on_skipped_pipeline", "analytics_access_level", "approvals_before_merge", "auto_cancel_pending_pipelines", "auto_devops_deploy_strategy", "auto_devops_enabled", "autoclose_referenced_issues", "avatar", "build_coverage_regex", "build_git_strategy", "build_timeout", "builds_access_level", "ci_config_path", "container_expiration_policy_attributes", "container_registry_enabled", "default_branch", "description", "emails_disabled", "external_authorization_classification_label", "forking_access_level", "group_with_project_templates_id", "import_url", "initialize_with_readme", "issues_access_level", "issues_enabled", "jobs_enabled", "lfs_enabled", "merge_method", "merge_requests_access_level", "merge_requests_enabled", "mirror_trigger_builds", "mirror", "namespace_id", "operations_access_level", "only_allow_merge_if_all_discussions_are_resolved", "only_allow_merge_if_pipeline_succeeds", "packages_enabled", "pages_access_level", "requirements_access_level", "printing_merge_request_link_enabled", "public_builds", "remove_source_branch_after_merge", "repository_access_level", "repository_storage", "request_access_enabled", "resolve_outdated_diff_discussions", "shared_runners_enabled", "show_default_award_emojis", "snippets_access_level", "snippets_enabled", "tag_list", "template_name", "template_project_id", "use_custom_template", "visibility", "wiki_access_level", "wiki_enabled", ), ) # Please keep these _update_attrs in same order as they are at: # https://docs.gitlab.com/ee/api/projects.html#edit-project _update_attrs = RequiredOptional( optional=( "allow_merge_on_skipped_pipeline", "analytics_access_level", "approvals_before_merge", "auto_cancel_pending_pipelines", "auto_devops_deploy_strategy", "auto_devops_enabled", "autoclose_referenced_issues", "avatar", "build_coverage_regex", "build_git_strategy", "build_timeout", "builds_access_level", "ci_config_path", "ci_default_git_depth", "ci_forward_deployment_enabled", "container_expiration_policy_attributes", "container_registry_enabled", "default_branch", "description", "emails_disabled", "external_authorization_classification_label", "forking_access_level", "import_url", "issues_access_level", "issues_enabled", "jobs_enabled", "lfs_enabled", "merge_method", "merge_requests_access_level", "merge_requests_enabled", "mirror_overwrites_diverged_branches", "mirror_trigger_builds", "mirror_user_id", "mirror", "name", "operations_access_level", "only_allow_merge_if_all_discussions_are_resolved", "only_allow_merge_if_pipeline_succeeds", "only_mirror_protected_branches", "packages_enabled", "pages_access_level", "requirements_access_level", "restrict_user_defined_variables", "path", "public_builds", "remove_source_branch_after_merge", "repository_access_level", "repository_storage", "request_access_enabled", "resolve_outdated_diff_discussions", "service_desk_enabled", "shared_runners_enabled", "show_default_award_emojis", "snippets_access_level", "snippets_enabled", "suggestion_commit_message", "tag_list", "visibility", "wiki_access_level", "wiki_enabled", "issues_template", "merge_requests_template", ), ) _list_filters = ( "archived", "id_after", "id_before", "last_activity_after", "last_activity_before", "membership", "min_access_level", "order_by", "owned", "repository_checksum_failed", "repository_storage", "search_namespaces", "search", "simple", "sort", "starred", "statistics", "topic", "visibility", "wiki_checksum_failed", "with_custom_attributes", "with_issues_enabled", "with_merge_requests_enabled", "with_programming_language", ) _types = {"avatar": types.ImageAttribute, "topic": types.ListAttribute} def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> Project: return cast(Project, super().get(id=id, lazy=lazy, **kwargs)) def import_project( self, file: str, path: str, name: Optional[str] = None, namespace: Optional[str] = None, overwrite: bool = False, override_params: Optional[Dict[str, Any]] = None, **kwargs: Any ) -> Union[Dict[str, Any], requests.Response]: """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, "application/octet-stream")} data = {"path": path, "overwrite": str(overwrite)} if override_params: for k, v in override_params.items(): data["override_params[%s]" % k] = v if name is not None: data["name"] = name if namespace: data["namespace"] = namespace return self.gitlab.http_post( "/projects/import", post_data=data, files=files, **kwargs ) def import_bitbucket_server( self, bitbucket_server_url: str, bitbucket_server_username: str, personal_access_token: str, bitbucket_server_project: str, bitbucket_server_repo: str, new_name: Optional[str] = None, target_namespace: Optional[str] = None, **kwargs: Any ) -> Union[Dict[str, Any], requests.Response]: """Import a project from BitBucket Server 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: bitbucket_server_url (str): Bitbucket Server URL bitbucket_server_username (str): Bitbucket Server Username personal_access_token (str): Bitbucket Server personal access token/password bitbucket_server_project (str): Bitbucket Project Key bitbucket_server_repo (str): Bitbucket Repository Name new_name (str): New repository name (Optional) target_namespace (str): Namespace to import repository into. Supports subgroups like /namespace/subgroup (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: .. code-block:: python gl = gitlab.Gitlab_from_config() print("Triggering import") result = gl.projects.import_bitbucket_server( bitbucket_server_url="https://some.server.url", bitbucket_server_username="some_bitbucket_user", personal_access_token="my_password_or_access_token", bitbucket_server_project="my_project", bitbucket_server_repo="my_repo", new_name="gl_project_name", target_namespace="gl_project_path" ) 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("BitBucket import complete") """ data = { "bitbucket_server_url": bitbucket_server_url, "bitbucket_server_username": bitbucket_server_username, "personal_access_token": personal_access_token, "bitbucket_server_project": bitbucket_server_project, "bitbucket_server_repo": bitbucket_server_repo, } if new_name: data["new_name"] = new_name if target_namespace: data["target_namespace"] = target_namespace 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/bitbucket_server", post_data=data, **kwargs ) return result def import_github( self, personal_access_token: str, repo_id: int, target_namespace: str, new_name: Optional[str] = None, **kwargs: Any ) -> Union[Dict[str, Any], requests.Response]: """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: .. code-block:: python 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 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 = RequiredOptional(optional=("namespace",)) def create( self, data: Optional[Dict[str, Any]] = None, **kwargs: Any ) -> ProjectFork: """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 """ if TYPE_CHECKING: assert self.path is not None path = self.path[:-1] # drop the 's' return cast(ProjectFork, CreateMixin.create(self, data, path=path, **kwargs)) class ProjectRemoteMirror(SaveMixin, RESTObject): pass class ProjectRemoteMirrorManager(ListMixin, CreateMixin, UpdateMixin, RESTManager): _path = "/projects/%(project_id)s/remote_mirrors" _obj_cls = ProjectRemoteMirror _from_parent_attrs = {"project_id": "id"} _create_attrs = RequiredOptional( required=("url",), optional=("enabled", "only_protected_branches") ) _update_attrs = RequiredOptional(optional=("enabled", "only_protected_branches")) python-gitlab-2.10.1/gitlab/v4/objects/push_rules.py000066400000000000000000000023511416141341200223410ustar00rootroot00000000000000from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import ( CreateMixin, DeleteMixin, GetWithoutIdMixin, ObjectDeleteMixin, SaveMixin, UpdateMixin, ) __all__ = [ "ProjectPushRules", "ProjectPushRulesManager", ] 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 = RequiredOptional( optional=( "deny_delete_tag", "member_check", "prevent_secrets", "commit_message_regex", "branch_name_regex", "author_email_regex", "file_name_regex", "max_file_size", ), ) _update_attrs = RequiredOptional( optional=( "deny_delete_tag", "member_check", "prevent_secrets", "commit_message_regex", "branch_name_regex", "author_email_regex", "file_name_regex", "max_file_size", ), ) python-gitlab-2.10.1/gitlab/v4/objects/releases.py000066400000000000000000000024631416141341200217570ustar00rootroot00000000000000from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import CRUDMixin, ObjectDeleteMixin, SaveMixin __all__ = [ "ProjectRelease", "ProjectReleaseManager", "ProjectReleaseLink", "ProjectReleaseLinkManager", ] class ProjectRelease(SaveMixin, RESTObject): _id_attr = "tag_name" _managers = (("links", "ProjectReleaseLinkManager"),) class ProjectReleaseManager(CRUDMixin, RESTManager): _path = "/projects/%(project_id)s/releases" _obj_cls = ProjectRelease _from_parent_attrs = {"project_id": "id"} _create_attrs = RequiredOptional( required=("tag_name", "description"), optional=("name", "ref", "assets") ) _update_attrs = RequiredOptional( optional=("name", "description", "milestones", "released_at") ) class ProjectReleaseLink(ObjectDeleteMixin, SaveMixin, RESTObject): pass class ProjectReleaseLinkManager(CRUDMixin, RESTManager): _path = "/projects/%(project_id)s/releases/%(tag_name)s/assets/links" _obj_cls = ProjectReleaseLink _from_parent_attrs = {"project_id": "project_id", "tag_name": "tag_name"} _create_attrs = RequiredOptional( required=("name", "url"), optional=("filepath", "link_type") ) _update_attrs = RequiredOptional(optional=("name", "url", "filepath", "link_type")) python-gitlab-2.10.1/gitlab/v4/objects/repositories.py000066400000000000000000000202521416141341200226770ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ee/api/repositories.html Currently this module only contains repository-related methods for projects. """ from gitlab import cli from gitlab import exceptions as exc from gitlab import utils class RepositoryMixin: @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") @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) python-gitlab-2.10.1/gitlab/v4/objects/runners.py000066400000000000000000000102041416141341200216400ustar00rootroot00000000000000from gitlab import cli from gitlab import exceptions as exc from gitlab import types from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import ( CRUDMixin, ListMixin, NoUpdateMixin, ObjectDeleteMixin, SaveMixin, ) __all__ = [ "RunnerJob", "RunnerJobManager", "Runner", "RunnerManager", "GroupRunner", "GroupRunnerManager", "ProjectRunner", "ProjectRunnerManager", ] 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 _create_attrs = RequiredOptional( required=("token",), optional=( "description", "info", "active", "locked", "run_untagged", "tag_list", "access_level", "maximum_timeout", ), ) _update_attrs = RequiredOptional( optional=( "description", "active", "tag_list", "run_untagged", "locked", "access_level", "maximum_timeout", ), ) _list_filters = ("scope", "tag_list") _types = {"tag_list": types.ListAttribute} @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 obj = self.gitlab.http_list(path, query_data, **kwargs) return [self._obj_cls(self, item) for item in obj] @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 GroupRunner(ObjectDeleteMixin, RESTObject): pass class GroupRunnerManager(NoUpdateMixin, RESTManager): _path = "/groups/%(group_id)s/runners" _obj_cls = GroupRunner _from_parent_attrs = {"group_id": "id"} _create_attrs = RequiredOptional(required=("runner_id",)) _list_filters = ("scope", "tag_list") _types = {"tag_list": types.ListAttribute} 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 = RequiredOptional(required=("runner_id",)) _list_filters = ("scope", "tag_list") _types = {"tag_list": types.ListAttribute} python-gitlab-2.10.1/gitlab/v4/objects/services.py000066400000000000000000000233211416141341200217730ustar00rootroot00000000000000from gitlab import cli from gitlab.base import RESTManager, RESTObject from gitlab.mixins import ( DeleteMixin, GetMixin, ListMixin, ObjectDeleteMixin, SaveMixin, UpdateMixin, ) __all__ = [ "ProjectService", "ProjectServiceManager", ] class ProjectService(SaveMixin, ObjectDeleteMixin, RESTObject): pass class ProjectServiceManager(GetMixin, UpdateMixin, DeleteMixin, ListMixin, RESTManager): _path = "/projects/%(project_id)s/services" _from_parent_attrs = {"project_id": "id"} _obj_cls = ProjectService _service_attrs = { "asana": (("api_key",), ("restrict_to_branch", "push_events")), "assembla": (("token",), ("subdomain", "push_events")), "bamboo": ( ("bamboo_url", "build_key", "username", "password"), ("push_events",), ), "bugzilla": ( ("new_issue_url", "issues_url", "project_url"), ("description", "title", "push_events"), ), "buildkite": ( ("token", "project_url"), ("enable_ssl_verification", "push_events"), ), "campfire": (("token",), ("subdomain", "room", "push_events")), "circuit": ( ("webhook",), ( "notify_only_broken_pipelines", "branches_to_be_notified", "push_events", "issues_events", "confidential_issues_events", "merge_requests_events", "tag_push_events", "note_events", "confidential_note_events", "pipeline_events", "wiki_page_events", ), ), "custom-issue-tracker": ( ("new_issue_url", "issues_url", "project_url"), ("description", "title", "push_events"), ), "drone-ci": ( ("token", "drone_url"), ( "enable_ssl_verification", "push_events", "merge_requests_events", "tag_push_events", ), ), "emails-on-push": ( ("recipients",), ( "disable_diffs", "send_from_committer_email", "push_events", "tag_push_events", "branches_to_be_notified", ), ), "pipelines-email": ( ("recipients",), ( "add_pusher", "notify_only_broken_builds", "branches_to_be_notified", "notify_only_default_branch", "pipeline_events", ), ), "external-wiki": (("external_wiki_url",), tuple()), "flowdock": (("token",), ("push_events",)), "github": (("token", "repository_url"), ("static_context",)), "hangouts-chat": ( ("webhook",), ( "notify_only_broken_pipelines", "notify_only_default_branch", "branches_to_be_notified", "push_events", "issues_events", "confidential_issues_events", "merge_requests_events", "tag_push_events", "note_events", "confidential_note_events", "pipeline_events", "wiki_page_events", ), ), "hipchat": ( ("token",), ( "color", "notify", "room", "api_version", "server", "push_events", "issues_events", "confidential_issues_events", "merge_requests_events", "tag_push_events", "note_events", "confidential_note_events", "pipeline_events", ), ), "irker": ( ("recipients",), ( "default_irc_uri", "server_port", "server_host", "colorize_messages", "push_events", ), ), "jira": ( ( "url", "username", "password", ), ( "api_url", "active", "jira_issue_transition_id", "commit_events", "merge_requests_events", "comment_on_event_enabled", ), ), "slack-slash-commands": (("token",), tuple()), "mattermost-slash-commands": (("token",), ("username",)), "packagist": ( ("username", "token"), ("server", "push_events", "merge_requests_events", "tag_push_events"), ), "mattermost": ( ("webhook",), ( "username", "channel", "notify_only_broken_pipelines", "notify_only_default_branch", "branches_to_be_notified", "push_events", "issues_events", "confidential_issues_events", "merge_requests_events", "tag_push_events", "note_events", "confidential_note_events", "pipeline_events", "wiki_page_events", "push_channel", "issue_channel", "confidential_issue_channel" "merge_request_channel", "note_channel", "confidential_note_channel", "tag_push_channel", "pipeline_channel", "wiki_page_channel", ), ), "pivotaltracker": (("token",), ("restrict_to_branch", "push_events")), "prometheus": (("api_url",), tuple()), "pushover": ( ("api_key", "user_key", "priority"), ("device", "sound", "push_events"), ), "redmine": ( ("new_issue_url", "project_url", "issues_url"), ("description", "push_events"), ), "slack": ( ("webhook",), ( "username", "channel", "notify_only_broken_pipelines", "notify_only_default_branch", "branches_to_be_notified", "commit_events", "confidential_issue_channel", "confidential_issues_events", "confidential_note_channel", "confidential_note_events", "deployment_channel", "deployment_events", "issue_channel", "issues_events", "job_events", "merge_request_channel", "merge_requests_events", "note_channel", "note_events", "pipeline_channel", "pipeline_events", "push_channel", "push_events", "tag_push_channel", "tag_push_events", "wiki_page_channel", "wiki_page_events", ), ), "microsoft-teams": ( ("webhook",), ( "notify_only_broken_pipelines", "notify_only_default_branch", "branches_to_be_notified", "push_events", "issues_events", "confidential_issues_events", "merge_requests_events", "tag_push_events", "note_events", "confidential_note_events", "pipeline_events", "wiki_page_events", ), ), "teamcity": ( ("teamcity_url", "build_type", "username", "password"), ("push_events",), ), "jenkins": (("jenkins_url", "project_name"), ("username", "password")), "mock-ci": (("mock_service_url",), tuple()), "youtrack": (("issues_url", "project_url"), ("description", "push_events")), } 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()) python-gitlab-2.10.1/gitlab/v4/objects/settings.py000066400000000000000000000076421416141341200220200ustar00rootroot00000000000000from gitlab import exceptions as exc from gitlab import types from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import GetWithoutIdMixin, SaveMixin, UpdateMixin __all__ = [ "ApplicationSettings", "ApplicationSettingsManager", ] class ApplicationSettings(SaveMixin, RESTObject): _id_attr = None class ApplicationSettingsManager(GetWithoutIdMixin, UpdateMixin, RESTManager): _path = "/application/settings" _obj_cls = ApplicationSettings _update_attrs = RequiredOptional( optional=( "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", "disabled_oauth_sign_in_sources", "domain_whitelist", "domain_blacklist_enabled", "domain_blacklist", "domain_allowlist", "domain_denylist_enabled", "domain_denylist", "external_authorization_service_enabled", "external_authorization_service_url", "external_authorization_service_default_label", "external_authorization_service_timeout", "import_sources", "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", "asset_proxy_allowlist", "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", ), ) _types = { "asset_proxy_allowlist": types.ListAttribute, "disabled_oauth_sign_in_sources": types.ListAttribute, "domain_allowlist": types.ListAttribute, "domain_denylist": types.ListAttribute, "import_sources": types.ListAttribute, "restricted_visibility_levels": types.ListAttribute, } @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) python-gitlab-2.10.1/gitlab/v4/objects/sidekiq.py000066400000000000000000000052141416141341200216020ustar00rootroot00000000000000from gitlab import cli from gitlab import exceptions as exc from gitlab.base import RESTManager __all__ = [ "SidekiqManager", ] 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) python-gitlab-2.10.1/gitlab/v4/objects/snippets.py000066400000000000000000000106061416141341200220170ustar00rootroot00000000000000from gitlab import cli from gitlab import exceptions as exc from gitlab import utils from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import CRUDMixin, ObjectDeleteMixin, SaveMixin, UserAgentDetailMixin from .award_emojis import ProjectSnippetAwardEmojiManager # noqa: F401 from .discussions import ProjectSnippetDiscussionManager # noqa: F401 from .notes import ProjectSnippetNoteManager # noqa: F401 __all__ = [ "Snippet", "SnippetManager", "ProjectSnippet", "ProjectSnippetManager", ] 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 = RequiredOptional( required=("title", "file_name", "content"), optional=("lifetime", "visibility") ) _update_attrs = RequiredOptional( optional=("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 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 = RequiredOptional( required=("title", "file_name", "content", "visibility"), optional=("description",), ) _update_attrs = RequiredOptional( optional=("title", "file_name", "content", "visibility", "description"), ) python-gitlab-2.10.1/gitlab/v4/objects/statistics.py000066400000000000000000000026651416141341200223520ustar00rootroot00000000000000from gitlab.base import RESTManager, RESTObject from gitlab.mixins import GetWithoutIdMixin, RefreshMixin __all__ = [ "GroupIssuesStatistics", "GroupIssuesStatisticsManager", "ProjectAdditionalStatistics", "ProjectAdditionalStatisticsManager", "IssuesStatistics", "IssuesStatisticsManager", "ProjectIssuesStatistics", "ProjectIssuesStatisticsManager", ] 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 IssuesStatistics(RefreshMixin, RESTObject): _id_attr = None class IssuesStatisticsManager(GetWithoutIdMixin, RESTManager): _path = "/issues_statistics" _obj_cls = IssuesStatistics class GroupIssuesStatistics(RefreshMixin, RESTObject): _id_attr = None class GroupIssuesStatisticsManager(GetWithoutIdMixin, RESTManager): _path = "/groups/%(group_id)s/issues_statistics" _obj_cls = GroupIssuesStatistics _from_parent_attrs = {"group_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"} python-gitlab-2.10.1/gitlab/v4/objects/tags.py000066400000000000000000000050301416141341200211030ustar00rootroot00000000000000from gitlab import cli from gitlab import exceptions as exc from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import NoUpdateMixin, ObjectDeleteMixin __all__ = [ "ProjectTag", "ProjectTagManager", "ProjectProtectedTag", "ProjectProtectedTagManager", ] 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) from e 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) from e 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 = RequiredOptional( required=("tag_name", "ref"), optional=("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 = RequiredOptional( required=("name",), optional=("create_access_level",) ) python-gitlab-2.10.1/gitlab/v4/objects/templates.py000066400000000000000000000020051416141341200221420ustar00rootroot00000000000000from gitlab.base import RESTManager, RESTObject from gitlab.mixins import RetrieveMixin __all__ = [ "Dockerfile", "DockerfileManager", "Gitignore", "GitignoreManager", "Gitlabciyml", "GitlabciymlManager", "License", "LicenseManager", ] class Dockerfile(RESTObject): _id_attr = "name" class DockerfileManager(RetrieveMixin, RESTManager): _path = "/templates/dockerfiles" _obj_cls = Dockerfile 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 License(RESTObject): _id_attr = "key" class LicenseManager(RetrieveMixin, RESTManager): _path = "/templates/licenses" _obj_cls = License _list_filters = ("popular",) _optional_get_attrs = ("project", "fullname") python-gitlab-2.10.1/gitlab/v4/objects/todos.py000066400000000000000000000030701416141341200212770ustar00rootroot00000000000000from gitlab import cli from gitlab import exceptions as exc from gitlab.base import RESTManager, RESTObject from gitlab.mixins import DeleteMixin, ListMixin, ObjectDeleteMixin __all__ = [ "Todo", "TodoManager", ] 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 """ self.gitlab.http_post("/todos/mark_as_done", **kwargs) python-gitlab-2.10.1/gitlab/v4/objects/triggers.py000066400000000000000000000023251416141341200217770ustar00rootroot00000000000000from gitlab import cli from gitlab import exceptions as exc from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import CRUDMixin, ObjectDeleteMixin, SaveMixin __all__ = [ "ProjectTrigger", "ProjectTriggerManager", ] 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 = RequiredOptional(required=("description",)) _update_attrs = RequiredOptional(required=("description",)) python-gitlab-2.10.1/gitlab/v4/objects/users.py000066400000000000000000000342261416141341200213170ustar00rootroot00000000000000from gitlab import cli from gitlab import exceptions as exc from gitlab import types from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import ( CreateMixin, CRUDMixin, DeleteMixin, GetWithoutIdMixin, ListMixin, NoUpdateMixin, ObjectDeleteMixin, RetrieveMixin, SaveMixin, UpdateMixin, ) from .custom_attributes import UserCustomAttributeManager # noqa: F401 from .events import UserEventManager # noqa: F401 __all__ = [ "CurrentUserEmail", "CurrentUserEmailManager", "CurrentUserGPGKey", "CurrentUserGPGKeyManager", "CurrentUserKey", "CurrentUserKeyManager", "CurrentUserStatus", "CurrentUserStatusManager", "CurrentUser", "CurrentUserManager", "User", "UserManager", "ProjectUser", "ProjectUserManager", "UserEmail", "UserEmailManager", "UserActivities", "UserStatus", "UserStatusManager", "UserActivitiesManager", "UserGPGKey", "UserGPGKeyManager", "UserKey", "UserKeyManager", "UserIdentityProviderManager", "UserImpersonationToken", "UserImpersonationTokenManager", "UserMembership", "UserMembershipManager", "UserProject", "UserProjectManager", ] class CurrentUserEmail(ObjectDeleteMixin, RESTObject): _short_print_attr = "email" class CurrentUserEmailManager(RetrieveMixin, CreateMixin, DeleteMixin, RESTManager): _path = "/user/emails" _obj_cls = CurrentUserEmail _create_attrs = RequiredOptional(required=("email",)) class CurrentUserGPGKey(ObjectDeleteMixin, RESTObject): pass class CurrentUserGPGKeyManager(RetrieveMixin, CreateMixin, DeleteMixin, RESTManager): _path = "/user/gpg_keys" _obj_cls = CurrentUserGPGKey _create_attrs = RequiredOptional(required=("key",)) class CurrentUserKey(ObjectDeleteMixin, RESTObject): _short_print_attr = "title" class CurrentUserKeyManager(RetrieveMixin, CreateMixin, DeleteMixin, RESTManager): _path = "/user/keys" _obj_cls = CurrentUserKey _create_attrs = RequiredOptional(required=("title", "key")) class CurrentUserStatus(SaveMixin, RESTObject): _id_attr = None _short_print_attr = "message" class CurrentUserStatusManager(GetWithoutIdMixin, UpdateMixin, RESTManager): _path = "/user/status" _obj_cls = CurrentUserStatus _update_attrs = RequiredOptional(optional=("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 User(SaveMixin, ObjectDeleteMixin, RESTObject): _short_print_attr = "username" _managers = ( ("customattributes", "UserCustomAttributeManager"), ("emails", "UserEmailManager"), ("followers_users", "UserFollowersManager"), ("following_users", "UserFollowingManager"), ("events", "UserEventManager"), ("gpgkeys", "UserGPGKeyManager"), ("identityproviders", "UserIdentityProviderManager"), ("impersonationtokens", "UserImpersonationTokenManager"), ("keys", "UserKeyManager"), ("memberships", "UserMembershipManager"), ("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.GitlabFollowError) def follow(self, **kwargs): """Follow the user. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabFollowError: If the user could not be followed Returns: dict: The new object data (*not* a RESTObject) """ path = "/users/%s/follow" % self.id return self.manager.gitlab.http_post(path, **kwargs) @cli.register_custom_action("User") @exc.on_http_error(exc.GitlabUnfollowError) def unfollow(self, **kwargs): """Unfollow the user. Args: **kwargs: Extra options to send to the server (e.g. sudo) Raises: GitlabAuthenticationError: If authentication is not correct GitlabUnfollowError: If the user could not be followed Returns: dict: The new object data (*not* a RESTObject) """ path = "/users/%s/unfollow" % self.id return self.manager.gitlab.http_post(path, **kwargs) @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 = RequiredOptional( optional=( "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", "color_scheme_id", "theme_id", ), ) _update_attrs = RequiredOptional( required=("email", "username", "name"), optional=( "password", "skype", "linkedin", "twitter", "projects_limit", "extern_uid", "provider", "bio", "admin", "can_create_group", "website_url", "skip_reconfirmation", "external", "organization", "location", "avatar", "public_email", "private_profile", "color_scheme_id", "theme_id", ), ) _types = {"confirm": types.LowercaseStringAttribute, "avatar": types.ImageAttribute} 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", "skip_users") _types = {"skip_users": types.ListAttribute} 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 = RequiredOptional(required=("email",)) 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 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 = RequiredOptional(required=("key",)) 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 = RequiredOptional(required=("title", "key")) class UserIdentityProviderManager(DeleteMixin, RESTManager): """Manager for user identities. This manager does not actually manage objects but enables functionality for deletion of user identities by provider. """ _path = "/users/%(user_id)s/identities" _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 = RequiredOptional( required=("name", "scopes"), optional=("expires_at",) ) _list_filters = ("state",) class UserMembership(RESTObject): _id_attr = "source_id" class UserMembershipManager(RetrieveMixin, RESTManager): _path = "/users/%(user_id)s/memberships" _obj_cls = UserMembership _from_parent_attrs = {"user_id": "id"} _list_filters = ("type",) # Having this outside projects avoids circular imports due to ProjectUser 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 = RequiredOptional( required=("name",), optional=( "default_branch", "issues_enabled", "wall_enabled", "merge_requests_enabled", "wiki_enabled", "snippets_enabled", "public", "visibility", "description", "builds_enabled", "public_builds", "import_url", "only_allow_merge_if_build_succeeds", ), ) _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 UserFollowersManager(ListMixin, RESTManager): _path = "/users/%(user_id)s/followers" _obj_cls = User _from_parent_attrs = {"user_id": "id"} class UserFollowingManager(ListMixin, RESTManager): _path = "/users/%(user_id)s/following" _obj_cls = User _from_parent_attrs = {"user_id": "id"} python-gitlab-2.10.1/gitlab/v4/objects/variables.py000066400000000000000000000037701416141341200221260ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ee/api/instance_level_ci_variables.html https://docs.gitlab.com/ee/api/project_level_variables.html https://docs.gitlab.com/ee/api/group_level_variables.html """ from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import CRUDMixin, ObjectDeleteMixin, SaveMixin __all__ = [ "Variable", "VariableManager", "GroupVariable", "GroupVariableManager", "ProjectVariable", "ProjectVariableManager", ] class Variable(SaveMixin, ObjectDeleteMixin, RESTObject): _id_attr = "key" class VariableManager(CRUDMixin, RESTManager): _path = "/admin/ci/variables" _obj_cls = Variable _create_attrs = RequiredOptional( required=("key", "value"), optional=("protected", "variable_type", "masked") ) _update_attrs = RequiredOptional( required=("key", "value"), optional=("protected", "variable_type", "masked") ) 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 = RequiredOptional( required=("key", "value"), optional=("protected", "variable_type", "masked") ) _update_attrs = RequiredOptional( required=("key", "value"), optional=("protected", "variable_type", "masked") ) 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 = RequiredOptional( required=("key", "value"), optional=("protected", "variable_type", "masked", "environment_scope"), ) _update_attrs = RequiredOptional( required=("key", "value"), optional=("protected", "variable_type", "masked", "environment_scope"), ) python-gitlab-2.10.1/gitlab/v4/objects/wikis.py000066400000000000000000000023211416141341200212730ustar00rootroot00000000000000from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import CRUDMixin, ObjectDeleteMixin, SaveMixin __all__ = [ "ProjectWiki", "ProjectWikiManager", "GroupWiki", "GroupWikiManager", ] 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 = RequiredOptional( required=("title", "content"), optional=("format",) ) _update_attrs = RequiredOptional(optional=("title", "content", "format")) _list_filters = ("with_content",) class GroupWiki(SaveMixin, ObjectDeleteMixin, RESTObject): _id_attr = "slug" _short_print_attr = "slug" class GroupWikiManager(CRUDMixin, RESTManager): _path = "/groups/%(group_id)s/wikis" _obj_cls = GroupWiki _from_parent_attrs = {"group_id": "id"} _create_attrs = RequiredOptional( required=("title", "content"), optional=("format",) ) _update_attrs = RequiredOptional(optional=("title", "content", "format")) _list_filters = ("with_content",) python-gitlab-2.10.1/pyproject.toml000066400000000000000000000012541416141341200172670ustar00rootroot00000000000000[tool.isort] profile = "black" multi_line_output = 3 order_by_type = false [tool.mypy] disallow_incomplete_defs = true disallow_untyped_defs = true files = "." [[tool.mypy.overrides]] # Overrides for currently untyped modules module = [ "docs.*", "docs.ext.*", "gitlab.v4.objects.*", "setup", "tests.functional.*", "tests.functional.api.*", "tests.unit.*" ] ignore_errors = true [[tool.mypy.overrides]] # Overrides to negate above patterns module = [ "gitlab.v4.objects.projects" ] ignore_errors = false [tool.semantic_release] version_variable = "gitlab/__version__.py:__version__" commit_subject = "chore: release v{version}" commit_message = "" python-gitlab-2.10.1/requirements-docker.txt000066400000000000000000000002421416141341200211000ustar00rootroot00000000000000-r requirements.txt -r requirements-test.txt docker-compose==1.29.2 # prevent inconsistent .env behavior from system install pytest-console-scripts pytest-docker python-gitlab-2.10.1/requirements-docs.txt000066400000000000000000000001241416141341200205600ustar00rootroot00000000000000-r requirements.txt jinja2 sphinx==3.5.4 sphinx_rtd_theme sphinxcontrib-autoprogram python-gitlab-2.10.1/requirements-lint.txt000066400000000000000000000001401416141341200205740ustar00rootroot00000000000000black==20.8b1 flake8==3.9.2 isort==5.9.3 mypy==0.910 types-PyYAML==5.4.8 types-requests==2.25.6 python-gitlab-2.10.1/requirements-test.txt000066400000000000000000000000621416141341200206100ustar00rootroot00000000000000coverage httmock mock pytest pytest-cov responses python-gitlab-2.10.1/requirements.txt000066400000000000000000000000521416141341200176320ustar00rootroot00000000000000requests==2.26.0 requests-toolbelt==0.9.1 python-gitlab-2.10.1/setup.cfg000066400000000000000000000000631416141341200161710ustar00rootroot00000000000000[build_sphinx] warning-is-error = 1 keep-going = 1 python-gitlab-2.10.1/setup.py000066400000000000000000000032751416141341200160720ustar00rootroot00000000000000#!/usr/bin/python # -*- coding: utf-8 -*- from setuptools import find_packages, setup def get_version(): with open("gitlab/__version__.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, long_description_content_type="text/x-rst", 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.25.0", "requests-toolbelt>=0.9.1"], package_data={ "gitlab": ["py.typed"], }, 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", "Programming Language :: Python :: 3.9", ], extras_require={ "autocompletion": ["argcomplete>=1.10.0,<2"], "yaml": ["PyYaml>=5.2"], }, ) python-gitlab-2.10.1/tests/000077500000000000000000000000001416141341200155135ustar00rootroot00000000000000python-gitlab-2.10.1/tests/__init__.py000066400000000000000000000000001416141341200176120ustar00rootroot00000000000000python-gitlab-2.10.1/tests/functional/000077500000000000000000000000001416141341200176555ustar00rootroot00000000000000python-gitlab-2.10.1/tests/functional/__init__.py000066400000000000000000000000001416141341200217540ustar00rootroot00000000000000python-gitlab-2.10.1/tests/functional/api/000077500000000000000000000000001416141341200204265ustar00rootroot00000000000000python-gitlab-2.10.1/tests/functional/api/__init__.py000066400000000000000000000000001416141341200225250ustar00rootroot00000000000000python-gitlab-2.10.1/tests/functional/api/test_clusters.py000066400000000000000000000023441416141341200237060ustar00rootroot00000000000000def test_project_clusters(project): project.clusters.create( { "name": "cluster1", "platform_kubernetes_attributes": { "api_url": "http://url", "token": "tokenval", }, } ) clusters = project.clusters.list() assert len(clusters) == 1 cluster = clusters[0] cluster.platform_kubernetes_attributes = {"api_url": "http://newurl"} cluster.save() cluster = project.clusters.list()[0] assert cluster.platform_kubernetes["api_url"] == "http://newurl" cluster.delete() assert len(project.clusters.list()) == 0 def test_group_clusters(group): group.clusters.create( { "name": "cluster1", "platform_kubernetes_attributes": { "api_url": "http://url", "token": "tokenval", }, } ) clusters = group.clusters.list() assert len(clusters) == 1 cluster = clusters[0] cluster.platform_kubernetes_attributes = {"api_url": "http://newurl"} cluster.save() cluster = group.clusters.list()[0] assert cluster.platform_kubernetes["api_url"] == "http://newurl" cluster.delete() assert len(group.clusters.list()) == 0 python-gitlab-2.10.1/tests/functional/api/test_current_user.py000066400000000000000000000020451416141341200245600ustar00rootroot00000000000000def test_current_user_email(gl): gl.auth() 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 def test_current_user_gpg_keys(gl, GPG_KEY): gl.auth() 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 def test_current_user_ssh_keys(gl, SSH_KEY): gl.auth() 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 def test_current_user_status(gl): gl.auth() 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.10.1/tests/functional/api/test_deploy_keys.py000066400000000000000000000007221416141341200243670ustar00rootroot00000000000000def test_project_deploy_keys(gl, project, DEPLOY_KEY): deploy_key = project.keys.create({"title": "foo@bar", "key": DEPLOY_KEY}) project_keys = list(project.keys.list()) assert len(project_keys) == 1 project2 = gl.projects.create({"name": "deploy-key-project"}) project2.keys.enable(deploy_key.id) assert len(project2.keys.list()) == 1 project2.keys.delete(deploy_key.id) assert len(project2.keys.list()) == 0 project2.delete() python-gitlab-2.10.1/tests/functional/api/test_deploy_tokens.py000066400000000000000000000022261416141341200247200ustar00rootroot00000000000000def test_project_deploy_tokens(gl, project): deploy_token = project.deploytokens.create( { "name": "foo", "username": "bar", "expires_at": "2022-01-01", "scopes": ["read_registry"], } ) assert len(project.deploytokens.list()) == 1 assert gl.deploytokens.list() == project.deploytokens.list() assert project.deploytokens.list()[0].name == "foo" assert project.deploytokens.list()[0].expires_at == "2022-01-01T00:00:00.000Z" assert project.deploytokens.list()[0].scopes == ["read_registry"] assert project.deploytokens.list()[0].username == "bar" deploy_token.delete() assert len(project.deploytokens.list()) == 0 assert len(gl.deploytokens.list()) == 0 def test_group_deploy_tokens(gl, group): deploy_token = group.deploytokens.create( { "name": "foo", "scopes": ["read_registry"], } ) assert len(group.deploytokens.list()) == 1 assert gl.deploytokens.list() == group.deploytokens.list() deploy_token.delete() assert len(group.deploytokens.list()) == 0 assert len(gl.deploytokens.list()) == 0 python-gitlab-2.10.1/tests/functional/api/test_gitlab.py000066400000000000000000000110461416141341200233030ustar00rootroot00000000000000import pytest import gitlab def test_auth_from_config(gl, temp_dir): """Test token authentication from config file""" test_gitlab = gitlab.Gitlab.from_config( config_files=[temp_dir / "python-gitlab.cfg"] ) test_gitlab.auth() assert isinstance(test_gitlab.user, gitlab.v4.objects.CurrentUser) def test_broadcast_messages(gl): 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 def test_markdown(gl): html = gl.markdown("foo") assert "foo" in html def test_lint(gl): success, errors = gl.lint("Invalid") assert success is False assert errors def test_sidekiq_queue_metrics(gl): out = gl.sidekiq.queue_metrics() assert isinstance(out, dict) assert "pages" in out["queues"] def test_sidekiq_process_metrics(gl): out = gl.sidekiq.process_metrics() assert isinstance(out, dict) assert "hostname" in out["processes"][0] def test_sidekiq_job_stats(gl): out = gl.sidekiq.job_stats() assert isinstance(out, dict) assert "processed" in out["jobs"] def test_sidekiq_compound_metrics(gl): out = gl.sidekiq.compound_metrics() assert isinstance(out, dict) assert "jobs" in out assert "processes" in out assert "queues" in out def test_gitlab_settings(gl): settings = gl.settings.get() settings.default_projects_limit = 42 settings.save() settings = gl.settings.get() assert settings.default_projects_limit == 42 def test_template_dockerfile(gl): assert gl.dockerfiles.list() dockerfile = gl.dockerfiles.get("Node") assert dockerfile.content is not None def test_template_gitignore(gl): assert gl.gitignores.list() gitignore = gl.gitignores.get("Node") assert gitignore.content is not None def test_template_gitlabciyml(gl): assert gl.gitlabciymls.list() gitlabciyml = gl.gitlabciymls.get("Nodejs") assert gitlabciyml.content is not None def test_template_license(gl): assert gl.licenses.list() license = gl.licenses.get( "bsd-2-clause", project="mytestproject", fullname="mytestfullname" ) assert "mytestfullname" in license.content def test_hooks(gl): hook = gl.hooks.create({"url": "http://whatever.com"}) assert len(gl.hooks.list()) == 1 hook.delete() assert len(gl.hooks.list()) == 0 def test_namespaces(gl): namespace = gl.namespaces.list(all=True) assert namespace namespace = gl.namespaces.list(search="root", all=True)[0] assert namespace.kind == "user" def test_notification_settings(gl): settings = gl.notificationsettings.get() settings.level = gitlab.NOTIFICATION_LEVEL_WATCH settings.save() settings = gl.notificationsettings.get() assert settings.level == gitlab.NOTIFICATION_LEVEL_WATCH def test_user_activities(gl): activities = gl.user_activities.list(query_parameters={"from": "2019-01-01"}) assert isinstance(activities, list) def test_events(gl): events = gl.events.list() assert isinstance(events, list) @pytest.mark.skip def test_features(gl): feat = gl.features.set("foo", 30) assert feat.name == "foo" assert len(gl.features.list()) == 1 feat.delete() assert len(gl.features.list()) == 0 def test_pagination(gl, project): project2 = gl.projects.create({"name": "project-page-2"}) list1 = gl.projects.list(per_page=1, page=1) list2 = gl.projects.list(per_page=1, page=2) assert len(list1) == 1 assert len(list2) == 1 assert list1[0].id != list2[0].id project2.delete() def test_rate_limits(gl): 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"})) with pytest.raises(gitlab.GitlabCreateError) as e: for i in range(20, 40): projects.append( gl.projects.create( {"name": str(i) + "shouldfail"}, obey_rate_limit=False ) ) assert "Retry later" in str(e.value) settings.throttle_authenticated_api_enabled = False settings.save() [project.delete() for project in projects] python-gitlab-2.10.1/tests/functional/api/test_groups.py000066400000000000000000000156721416141341200233710ustar00rootroot00000000000000import pytest import gitlab def test_groups(gl): # TODO: This one still needs lots of work user = gl.users.create( { "email": "user@test.com", "username": "user", "name": "user", "password": "user_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}) group4 = gl.groups.create({"name": "group4", "path": "group4"}) assert len(gl.groups.list()) == 4 assert len(gl.groups.list(search="oup1")) == 1 assert group3.parent_id == p_id assert group2.subgroups.list()[0].id == group3.id assert group2.descendant_groups.list()[0].id == group3.id filtered_groups = gl.groups.list(skip_groups=[group3.id, group4.id]) assert group3 not in filtered_groups assert group3 not in filtered_groups group1.members.create( {"access_level": gitlab.const.OWNER_ACCESS, "user_id": user.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} ) group4.share(group1.id, gitlab.const.DEVELOPER_ACCESS) group4.share(group2.id, gitlab.const.MAINTAINER_ACCESS) # Reload group4 to have updated shared_with_groups group4 = gl.groups.get(group4.id) assert len(group4.shared_with_groups) == 2 group4.unshare(group1.id) # Reload group4 to have updated shared_with_groups group4 = gl.groups.get(group4.id) assert len(group4.shared_with_groups) == 1 # User memberships (admin only) memberships1 = user.memberships.list() assert len(memberships1) == 1 memberships2 = user2.memberships.list() assert len(memberships2) == 2 membership = memberships1[0] assert membership.source_type == "Namespace" assert membership.access_level == gitlab.const.OWNER_ACCESS project_memberships = user.memberships.list(type="Project") assert len(project_memberships) == 0 group_memberships = user.memberships.list(type="Namespace") assert len(group_memberships) == 1 with pytest.raises(gitlab.GitlabListError) as e: membership = user.memberships.list(type="Invalid") assert "type does not have a valid value" in str(e.value) with pytest.raises(gitlab.GitlabListError) as e: user.memberships.list(sudo=user.name) assert "403 Forbidden" in str(e.value) # Administrator belongs to the groups assert len(group1.members.list()) == 3 assert len(group2.members.list()) == 2 group1.members.delete(user.id) assert len(group1.members.list()) == 2 assert len(group1.members.all()) # Deprecated assert len(group1.members_all.list()) 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) @pytest.mark.skip(reason="Commented out in legacy test") def test_group_labels(group): group.labels.create({"name": "foo", "description": "bar", "color": "#112233"}) label = group.labels.get("foo") assert label.description == "bar" label.description = "baz" label.save() label = group.labels.get("foo") assert label.description == "baz" assert len(group.labels.list()) == 1 label.delete() assert len(group.labels.list()) == 0 def test_group_notification_settings(group): settings = group.notificationsettings.get() settings.level = "disabled" settings.save() settings = group.notificationsettings.get() assert settings.level == "disabled" def test_group_badges(group): badge_image = "http://example.com" badge_link = "http://example/img.svg" badge = group.badges.create({"link_url": badge_link, "image_url": badge_image}) assert len(group.badges.list()) == 1 badge.image_url = "http://another.example.com" badge.save() badge = group.badges.get(badge.id) assert badge.image_url == "http://another.example.com" badge.delete() assert len(group.badges.list()) == 0 def test_group_milestones(group): milestone = group.milestones.create({"title": "groupmilestone1"}) assert len(group.milestones.list()) == 1 milestone.due_date = "2020-01-01T00:00:00Z" milestone.save() milestone.state_event = "close" milestone.save() milestone = group.milestones.get(milestone.id) assert milestone.state == "closed" assert len(milestone.issues()) == 0 assert len(milestone.merge_requests()) == 0 def test_group_custom_attributes(gl, group): attrs = group.customattributes.list() assert len(attrs) == 0 attr = group.customattributes.set("key", "value1") assert len(gl.groups.list(custom_attributes={"key": "value1"})) == 1 assert attr.key == "key" assert attr.value == "value1" assert len(group.customattributes.list()) == 1 attr = group.customattributes.set("key", "value2") attr = group.customattributes.get("key") assert attr.value == "value2" assert len(group.customattributes.list()) == 1 attr.delete() assert len(group.customattributes.list()) == 0 def test_group_subgroups_projects(gl, user): # TODO: fixture factories group1 = gl.groups.list(search="group1")[0] group2 = gl.groups.list(search="group2")[0] group3 = gl.groups.create( {"name": "subgroup1", "path": "subgroup1", "parent_id": group1.id} ) group4 = gl.groups.create( {"name": "subgroup2", "path": "subgroup2", "parent_id": group2.id} ) gr1_project = gl.projects.create({"name": "gr1_project", "namespace_id": group1.id}) gr2_project = gl.projects.create({"name": "gr2_project", "namespace_id": group3.id}) assert group3.parent_id == group1.id assert group4.parent_id == group2.id assert gr1_project.namespace["id"] == group1.id assert gr2_project.namespace["parent_id"] == group1.id @pytest.mark.skip def test_group_wiki(group): content = "Group Wiki page content" wiki = group.wikis.create({"title": "groupwikipage", "content": content}) assert len(group.wikis.list()) == 1 wiki = group.wikis.get(wiki.slug) assert wiki.content == content wiki.content = "new content" wiki.save() wiki.delete() assert len(group.wikis.list()) == 0 @pytest.mark.skip(reason="EE feature") def test_group_hooks(group): hook = group.hooks.create({"url": "http://hook.url"}) assert len(group.hooks.list()) == 1 hook.note_events = True hook.save() hook = group.hooks.get(hook.id) assert hook.note_events is True hook.delete() python-gitlab-2.10.1/tests/functional/api/test_import_export.py000066400000000000000000000037071416141341200247610ustar00rootroot00000000000000import time import gitlab def test_group_import_export(gl, group, temp_dir): export = group.exports.create() assert export.message == "202 Accepted" # We cannot check for export_status with group export API time.sleep(10) import_archive = temp_dir / "gitlab-group-export.tgz" import_path = "imported_group" import_name = "Imported Group" with open(import_archive, "wb") as f: export.download(streamed=True, action=f.write) with open(import_archive, "rb") as f: output = gl.groups.import_group(f, import_path, import_name) assert output["message"] == "202 Accepted" # We cannot check for returned ID with group import API time.sleep(10) group_import = gl.groups.get(import_path) assert group_import.path == import_path assert group_import.name == import_name def test_project_import_export(gl, project, temp_dir): export = project.exports.create() assert export.message == "202 Accepted" export = project.exports.get() assert isinstance(export, gitlab.v4.objects.ProjectExport) count = 0 while export.export_status != "finished": time.sleep(1) export.refresh() count += 1 if count == 15: raise Exception("Project export taking too much time") with open(temp_dir / "gitlab-export.tgz", "wb") as f: export.download(streamed=True, action=f.write) output = gl.projects.import_project( open(temp_dir / "gitlab-export.tgz", "rb"), "imported_project", name="Imported Project", ) project_import = gl.projects.get(output["id"], lazy=True).imports.get() assert project_import.path == "imported_project" assert project_import.name == "Imported Project" count = 0 while project_import.import_status != "finished": time.sleep(1) project_import.refresh() count += 1 if count == 15: raise Exception("Project import taking too much time") python-gitlab-2.10.1/tests/functional/api/test_issues.py000066400000000000000000000057711416141341200233640ustar00rootroot00000000000000import gitlab def test_create_issue(project): issue = project.issues.create({"title": "my issue 1"}) issue2 = project.issues.create({"title": "my issue 2"}) issue_iids = [issue.iid for issue in project.issues.list()] assert len(issue_iids) == 2 # Test 'iids' as a list assert len(project.issues.list(iids=issue_iids)) == 2 issue2.state_event = "close" issue2.save() assert len(project.issues.list(state="closed")) == 1 assert len(project.issues.list(state="opened")) == 1 assert isinstance(issue.user_agent_detail(), dict) assert issue.user_agent_detail()["user_agent"] assert issue.participants() assert type(issue.closed_by()) == list assert type(issue.related_merge_requests()) == list def test_issue_notes(issue): size = len(issue.notes.list()) note = issue.notes.create({"body": "This is an issue note"}) assert len(issue.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(issue.notes.list()) == size def test_issue_labels(project, issue): project.labels.create({"name": "label2", "color": "#aabbcc"}) issue.labels = ["label2"] issue.save() assert issue in project.issues.list(labels=["label2"]) assert issue in project.issues.list(labels="label2") assert issue in project.issues.list(labels="Any") assert issue not in project.issues.list(labels="None") def test_issue_events(issue): events = issue.resourcelabelevents.list() assert isinstance(events, list) event = issue.resourcelabelevents.get(events[0].id) assert isinstance(event, gitlab.v4.objects.ProjectIssueResourceLabelEvent) def test_issue_milestones(project, milestone): data = {"title": "my issue 1", "milestone_id": milestone.id} issue = project.issues.create(data) assert milestone.issues().next().title == "my issue 1" milestone_events = issue.resourcemilestoneevents.list() assert isinstance(milestone_events, list) milestone_event = issue.resourcemilestoneevents.get(milestone_events[0].id) assert isinstance( milestone_event, gitlab.v4.objects.ProjectIssueResourceMilestoneEvent ) milestone_issues = project.issues.list(milestone=milestone.title) assert len(milestone_issues) == 1 def test_issue_discussions(issue): size = len(issue.discussions.list()) discussion = issue.discussions.create({"body": "Discussion body"}) assert len(issue.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 = issue.discussions.get(discussion.id) assert discussion.attributes["notes"][-1]["body"] == "updated body" d_note_from_get.delete() discussion = issue.discussions.get(discussion.id) assert len(discussion.attributes["notes"]) == 1 python-gitlab-2.10.1/tests/functional/api/test_keys.py000066400000000000000000000023331416141341200230130ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ce/api/keys.html """ import base64 import hashlib def key_fingerprint(key): key_part = key.split()[1] decoded = base64.b64decode(key_part.encode("ascii")) digest = hashlib.sha256(decoded).digest() return "SHA256:" + base64.b64encode(digest).rstrip(b"=").decode("utf-8") def test_keys_ssh(gl, user, SSH_KEY): key = user.keys.create({"title": "foo@bar", "key": SSH_KEY}) # Get key by ID (admin only). key_by_id = gl.keys.get(key.id) assert key_by_id.title == key.title assert key_by_id.key == key.key fingerprint = key_fingerprint(SSH_KEY) # Get key by fingerprint (admin only). key_by_fingerprint = gl.keys.get(fingerprint=fingerprint) assert key_by_fingerprint.title == key.title assert key_by_fingerprint.key == key.key key.delete() def test_keys_deploy(gl, project, DEPLOY_KEY): key = project.keys.create({"title": "foo@bar", "key": DEPLOY_KEY}) fingerprint = key_fingerprint(DEPLOY_KEY) key_by_fingerprint = gl.keys.get(fingerprint=fingerprint) assert key_by_fingerprint.title == key.title assert key_by_fingerprint.key == key.key assert len(key_by_fingerprint.deploy_keys_projects) == 1 key.delete() python-gitlab-2.10.1/tests/functional/api/test_merge_requests.py000066400000000000000000000130631416141341200250740ustar00rootroot00000000000000import time import pytest import gitlab import gitlab.v4.objects def test_merge_requests(project): project.files.create( { "file_path": "README.rst", "branch": "master", "content": "Initial content", "commit_message": "Initial commit", } ) source_branch = "branch1" project.branches.create({"branch": source_branch, "ref": "master"}) project.files.create( { "file_path": "README2.rst", "branch": source_branch, "content": "Initial content", "commit_message": "New commit in new branch", } ) project.mergerequests.create( {"source_branch": "branch1", "target_branch": "master", "title": "MR readme2"} ) def test_merge_request_discussion(project): mr = project.mergerequests.list()[0] size = len(mr.discussions.list()) discussion = mr.discussions.create({"body": "Discussion body"}) assert len(mr.discussions.list()) == size + 1 note = discussion.notes.create({"body": "first note"}) note_from_get = discussion.notes.get(note.id) note_from_get.body = "updated body" note_from_get.save() discussion = mr.discussions.get(discussion.id) assert discussion.attributes["notes"][-1]["body"] == "updated body" note_from_get.delete() discussion = mr.discussions.get(discussion.id) assert len(discussion.attributes["notes"]) == 1 def test_merge_request_labels(project): mr = project.mergerequests.list()[0] mr.labels = ["label2"] mr.save() events = mr.resourcelabelevents.list() assert events event = mr.resourcelabelevents.get(events[0].id) assert event def test_merge_request_milestone_events(project, milestone): mr = project.mergerequests.list()[0] mr.milestone_id = milestone.id mr.save() milestones = mr.resourcemilestoneevents.list() assert milestones milestone = mr.resourcemilestoneevents.get(milestones[0].id) assert milestone def test_merge_request_basic(project): mr = project.mergerequests.list()[0] # basic testing: only make sure that the methods exist mr.commits() mr.changes() assert mr.participants() def test_merge_request_rebase(project): mr = project.mergerequests.list()[0] assert mr.rebase() @pytest.mark.skip(reason="flaky test") def test_merge_request_merge(project): mr = project.mergerequests.list()[0] mr.merge() project.branches.delete(mr.source_branch) with pytest.raises(gitlab.GitlabMRClosedError): # Two merge attempts should raise GitlabMRClosedError mr.merge() def test_merge_request_should_remove_source_branch( project, merge_request, wait_for_sidekiq ) -> None: """Test to ensure https://github.com/python-gitlab/python-gitlab/issues/1120 is fixed. Bug reported that they could not use 'should_remove_source_branch' in mr.merge() call""" source_branch = "remove_source_branch" mr = merge_request(source_branch=source_branch) mr.merge(should_remove_source_branch=True) result = wait_for_sidekiq(timeout=60) assert result is True, "sidekiq process should have terminated but did not" # Wait until it is merged mr_iid = mr.iid for _ in range(60): mr = project.mergerequests.get(mr_iid) if mr.merged_at is not None: break time.sleep(0.5) assert mr.merged_at is not None time.sleep(0.5) # Ensure we can NOT get the MR branch with pytest.raises(gitlab.exceptions.GitlabGetError): project.branches.get(source_branch) def test_merge_request_large_commit_message( project, merge_request, wait_for_sidekiq ) -> None: """Test to ensure https://github.com/python-gitlab/python-gitlab/issues/1452 is fixed. Bug reported that very long 'merge_commit_message' in mr.merge() would cause an error: 414 Request too large """ source_branch = "large_commit_message" mr = merge_request(source_branch=source_branch) merge_commit_message = "large_message\r\n" * 1_000 assert len(merge_commit_message) > 10_000 mr.merge(merge_commit_message=merge_commit_message) result = wait_for_sidekiq(timeout=60) assert result is True, "sidekiq process should have terminated but did not" # Wait until it is merged mr_iid = mr.iid for _ in range(60): mr = project.mergerequests.get(mr_iid) if mr.merged_at is not None: break time.sleep(0.5) assert mr.merged_at is not None time.sleep(0.5) # Ensure we can get the MR branch project.branches.get(source_branch) def test_merge_request_merge_ref(merge_request) -> None: source_branch = "merge_ref_test" mr = merge_request(source_branch=source_branch) response = mr.merge_ref() assert response and "commit_id" in response def test_merge_request_merge_ref_should_fail( project, merge_request, wait_for_sidekiq ) -> None: source_branch = "merge_ref_test2" mr = merge_request(source_branch=source_branch) # Create conflict project.files.create( { "file_path": f"README.{source_branch}", "branch": project.default_branch, "content": "Different initial content", "commit_message": "Another commit in main branch", } ) result = wait_for_sidekiq(timeout=60) assert result is True, "sidekiq process should have terminated but did not" # Check for non-existing merge_ref for MR with conflicts with pytest.raises(gitlab.exceptions.GitlabGetError): response = mr.merge_ref() assert "commit_id" not in response python-gitlab-2.10.1/tests/functional/api/test_packages.py000066400000000000000000000031361416141341200236200ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ce/api/packages.html https://docs.gitlab.com/ee/user/packages/generic_packages """ from gitlab.v4.objects import GenericPackage package_name = "hello-world" package_version = "v1.0.0" file_name = "hello.tar.gz" file_content = "package content" def test_list_project_packages(project): packages = project.packages.list() assert isinstance(packages, list) def test_list_group_packages(group): packages = group.packages.list() assert isinstance(packages, list) def test_upload_generic_package(tmp_path, project): path = tmp_path / file_name path.write_text(file_content) package = project.generic_packages.upload( package_name=package_name, package_version=package_version, file_name=file_name, path=path, ) assert isinstance(package, GenericPackage) assert package.message == "201 Created" def test_download_generic_package(project): package = project.generic_packages.download( package_name=package_name, package_version=package_version, file_name=file_name, ) assert isinstance(package, bytes) assert package.decode("utf-8") == file_content def test_download_generic_package_to_file(tmp_path, project): path = tmp_path / file_name with open(path, "wb") as f: project.generic_packages.download( package_name=package_name, package_version=package_version, file_name=file_name, streamed=True, action=f.write, ) with open(path, "r") as f: assert f.read() == file_content python-gitlab-2.10.1/tests/functional/api/test_projects.py000066400000000000000000000171771416141341200237050ustar00rootroot00000000000000import pytest import gitlab def test_create_project(gl, user): # Moved from group tests chunk in legacy tests, TODO cleanup admin_project = gl.projects.create({"name": "admin_project"}) assert isinstance(admin_project, gitlab.v4.objects.Project) assert len(gl.projects.list(search="admin")) == 1 sudo_project = gl.projects.create({"name": "sudo_project"}, sudo=user.id) created = gl.projects.list() created_gen = gl.projects.list(as_list=False) owned = gl.projects.list(owned=True) assert admin_project in created and sudo_project in created assert admin_project in owned and sudo_project not in owned assert len(created) == len(list(created_gen)) admin_project.delete() sudo_project.delete() def test_project_badges(project): badge_image = "http://example.com" badge_link = "http://example/img.svg" badge = project.badges.create({"link_url": badge_link, "image_url": badge_image}) assert len(project.badges.list()) == 1 badge.image_url = "http://another.example.com" badge.save() badge = project.badges.get(badge.id) assert badge.image_url == "http://another.example.com" badge.delete() assert len(project.badges.list()) == 0 @pytest.mark.skip(reason="Commented out in legacy test") def test_project_boards(project): boards = 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 def test_project_custom_attributes(gl, project): attrs = project.customattributes.list() assert len(attrs) == 0 attr = project.customattributes.set("key", "value1") assert attr.key == "key" assert attr.value == "value1" assert len(project.customattributes.list()) == 1 assert len(gl.projects.list(custom_attributes={"key": "value1"})) == 1 attr = project.customattributes.set("key", "value2") attr = project.customattributes.get("key") assert attr.value == "value2" assert len(project.customattributes.list()) == 1 attr.delete() assert len(project.customattributes.list()) == 0 def test_project_environments(project): project.environments.create( {"name": "env1", "external_url": "http://fake.env/whatever"} ) environments = project.environments.list() assert len(environments) == 1 environment = environments[0] environment.external_url = "http://new.env/whatever" environment.save() environment = project.environments.list()[0] assert environment.external_url == "http://new.env/whatever" environment.stop() environment.delete() assert len(project.environments.list()) == 0 def test_project_events(project): events = project.events.list() assert isinstance(events, list) def test_project_file_uploads(project): filename = "test.txt" file_contents = "testing contents" uploaded_file = 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"] ) def test_project_forks(gl, project, user): fork = project.forks.create({"namespace": user.username}) fork_project = gl.projects.get(fork.id) assert fork_project.forked_from_project["id"] == project.id forks = project.forks.list() assert fork.id in map(lambda fork_project: fork_project.id, forks) def test_project_hooks(project): hook = project.hooks.create({"url": "http://hook.url"}) assert len(project.hooks.list()) == 1 hook.note_events = True hook.save() hook = project.hooks.get(hook.id) assert hook.note_events is True hook.delete() def test_project_housekeeping(project): project.housekeeping() def test_project_labels(project): label = project.labels.create({"name": "label", "color": "#778899"}) labels = project.labels.list() assert len(labels) == 1 label = project.labels.get("label") assert label == labels[0] label.new_name = "labelupdated" label.save() assert label.name == "labelupdated" label.subscribe() assert label.subscribed is True label.unsubscribe() assert label.subscribed is False label.delete() assert len(project.labels.list()) == 0 def test_project_milestones(project): milestone = project.milestones.create({"title": "milestone1"}) assert len(project.milestones.list()) == 1 milestone.due_date = "2020-01-01T00:00:00Z" milestone.save() milestone.state_event = "close" milestone.save() milestone = project.milestones.get(milestone.id) assert milestone.state == "closed" assert len(milestone.issues()) == 0 assert len(milestone.merge_requests()) == 0 def test_project_pages_domains(gl, project): domain = project.pagesdomains.create({"domain": "foo.domain.com"}) assert len(project.pagesdomains.list()) == 1 assert len(gl.pagesdomains.list()) == 1 domain = project.pagesdomains.get("foo.domain.com") assert domain.domain == "foo.domain.com" domain.delete() assert len(project.pagesdomains.list()) == 0 def test_project_protected_branches(project): p_b = project.protectedbranches.create({"name": "*-stable"}) assert p_b.name == "*-stable" assert len(project.protectedbranches.list()) == 1 p_b = project.protectedbranches.get("*-stable") p_b.delete() assert len(project.protectedbranches.list()) == 0 def test_project_remote_mirrors(project): mirror_url = "http://gitlab.test/root/mirror.git" mirror = project.remote_mirrors.create({"url": mirror_url}) assert mirror.url == mirror_url mirror.enabled = True mirror.save() mirror = project.remote_mirrors.list()[0] assert isinstance(mirror, gitlab.v4.objects.ProjectRemoteMirror) assert mirror.url == mirror_url assert mirror.enabled is True def test_project_services(project): # Use 'update' to create a service as we don't have a 'create' method and # to add one is somewhat complicated so it hasn't been done yet. project.services.update("asana", api_key="foo") service = project.services.get("asana") assert service.active is True service.api_key = "whatever" service.save() service = project.services.get("asana") assert service.active is True service.delete() service = project.services.get("asana") assert service.active is False def test_project_stars(project): project.star() assert project.star_count == 1 project.unstar() assert project.star_count == 0 def test_project_tags(project, project_file): tag = project.tags.create({"tag_name": "v1.0", "ref": "master"}) assert len(project.tags.list()) == 1 tag.set_release_description("Description 1") tag.set_release_description("Description 2") assert tag.release["description"] == "Description 2" tag.delete() assert len(project.tags.list()) == 0 def test_project_triggers(project): trigger = project.triggers.create({"description": "trigger1"}) assert len(project.triggers.list()) == 1 trigger.delete() def test_project_wiki(project): content = "Wiki page content" wiki = project.wikis.create({"title": "wikipage", "content": content}) assert len(project.wikis.list()) == 1 wiki = project.wikis.get(wiki.slug) assert wiki.content == content # update and delete seem broken wiki.content = "new content" wiki.save() wiki.delete() assert len(project.wikis.list()) == 0 python-gitlab-2.10.1/tests/functional/api/test_releases.py000066400000000000000000000040201416141341200236360ustar00rootroot00000000000000release_name = "Demo Release" release_tag_name = "v1.2.3" release_description = "release notes go here" link_data = {"url": "https://example.com", "name": "link_name"} def test_create_project_release(project, project_file): project.refresh() # Gets us the current default branch release = project.releases.create( { "name": release_name, "tag_name": release_tag_name, "description": release_description, "ref": project.default_branch, } ) assert len(project.releases.list()) == 1 assert project.releases.get(release_tag_name) assert release.name == release_name assert release.tag_name == release_tag_name assert release.description == release_description def test_create_project_release_no_name(project, project_file): unnamed_release_tag_name = "v2.3.4" project.refresh() # Gets us the current default branch release = project.releases.create( { "tag_name": unnamed_release_tag_name, "description": release_description, "ref": project.default_branch, } ) assert len(project.releases.list()) >= 1 assert project.releases.get(unnamed_release_tag_name) assert release.tag_name == unnamed_release_tag_name assert release.description == release_description def test_update_save_project_release(project, release): updated_description = f"{release.description} updated" release.description = updated_description release.save() release = project.releases.get(release.tag_name) assert release.description == updated_description def test_delete_project_release(project, release): project.releases.delete(release.tag_name) assert release not in project.releases.list() def test_create_project_release_links(project, release): release.links.create(link_data) release = project.releases.get(release.tag_name) assert release.assets["links"][0]["url"] == link_data["url"] assert release.assets["links"][0]["name"] == link_data["name"] python-gitlab-2.10.1/tests/functional/api/test_repository.py000066400000000000000000000073111416141341200242600ustar00rootroot00000000000000import base64 import time import pytest import gitlab def test_repository_files(project): project.files.create( { "file_path": "README", "branch": "master", "content": "Initial content", "commit_message": "Initial commit", } ) readme = 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") project.files.create( { "file_path": "README.rst", "branch": "master", "content": "Initial content", "commit_message": "New commit", } ) readme = 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 = project.files.blame(file_path="README.rst", ref="master") assert blame def test_repository_tree(project): tree = project.repository_tree() assert tree assert tree[0]["name"] == "README.rst" blob_id = tree[0]["id"] blob = project.repository_raw_blob(blob_id) assert blob.decode() == "Initial content" archive = project.repository_archive() assert isinstance(archive, bytes) archive2 = project.repository_archive("master") assert archive == archive2 snapshot = project.snapshot() assert isinstance(snapshot, bytes) def test_create_commit(project): data = { "branch": "master", "commit_message": "blah blah blah", "actions": [{"action": "create", "file_path": "blah", "content": "blah"}], } commit = project.commits.create(data) assert "@@" in project.commits.list()[0].diff()[0]["diff"] assert isinstance(commit.refs(), list) assert isinstance(commit.merge_requests(), list) def test_create_commit_status(project): commit = project.commits.list()[0] size = len(commit.statuses.list()) commit.statuses.create({"state": "success", "sha": commit.id}) assert len(commit.statuses.list()) == size + 1 def test_commit_signature(project): commit = project.commits.list()[0] with pytest.raises(gitlab.GitlabGetError) as e: commit.signature() assert "404 Signature Not Found" in str(e.value) def test_commit_comment(project): commit = project.commits.list()[0] commit.comments.create({"note": "This is a commit comment"}) assert len(commit.comments.list()) == 1 def test_commit_discussion(project): commit = project.commits.list()[0] count = len(commit.discussions.list()) discussion = commit.discussions.create({"body": "Discussion body"}) assert len(commit.discussions.list()) == (count + 1) note = discussion.notes.create({"body": "first note"}) note_from_get = discussion.notes.get(note.id) note_from_get.body = "updated body" note_from_get.save() discussion = commit.discussions.get(discussion.id) # assert discussion.attributes["notes"][-1]["body"] == "updated body" note_from_get.delete() discussion = commit.discussions.get(discussion.id) # assert len(discussion.attributes["notes"]) == 1 def test_revert_commit(project): commit = project.commits.list()[0] revert_commit = commit.revert(branch="master") expected_message = 'Revert "{}"\n\nThis reverts commit {}'.format( commit.message, commit.id ) assert revert_commit["message"] == expected_message with pytest.raises(gitlab.GitlabRevertError): # Two revert attempts should raise GitlabRevertError commit.revert(branch="master") python-gitlab-2.10.1/tests/functional/api/test_snippets.py000066400000000000000000000041251416141341200237060ustar00rootroot00000000000000import gitlab def test_snippets(gl): 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 def test_project_snippets(project): project.snippets_enabled = True project.save() snippet = 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"] def test_project_snippet_discussion(project): snippet = project.snippets.list()[0] size = len(snippet.discussions.list()) discussion = snippet.discussions.create({"body": "Discussion body"}) assert len(snippet.discussions.list()) == size + 1 note = discussion.notes.create({"body": "first note"}) note_from_get = discussion.notes.get(note.id) note_from_get.body = "updated body" note_from_get.save() discussion = snippet.discussions.get(discussion.id) assert discussion.attributes["notes"][-1]["body"] == "updated body" note_from_get.delete() discussion = snippet.discussions.get(discussion.id) assert len(discussion.attributes["notes"]) == 1 def test_project_snippet_file(project): snippet = project.snippets.list()[0] snippet.file_name = "bar.py" snippet.save() snippet = project.snippets.get(snippet.id) assert snippet.content().decode() == "initial content" assert snippet.file_name == "bar.py" size = len(project.snippets.list()) snippet.delete() assert len(project.snippets.list()) == (size - 1) python-gitlab-2.10.1/tests/functional/api/test_users.py000066400000000000000000000107751416141341200232120ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ee/api/users.html https://docs.gitlab.com/ee/api/users.html#delete-authentication-identity-from-user """ import pytest import requests @pytest.fixture(scope="session") def avatar_path(test_dir): return test_dir / "fixtures" / "avatar.png" def test_create_user(gl, avatar_path): user = gl.users.create( { "email": "foo@bar.com", "username": "foo", "name": "foo", "password": "foo_password", "avatar": open(avatar_path, "rb"), } ) created_user = gl.users.list(username="foo")[0] assert created_user.username == user.username assert created_user.email == user.email avatar_url = user.avatar_url.replace("gitlab.test", "localhost:8080") uploaded_avatar = requests.get(avatar_url).content assert uploaded_avatar == open(avatar_path, "rb").read() def test_block_user(gl, user): user.block() users = gl.users.list(blocked=True) assert user in users user.unblock() users = gl.users.list(blocked=False) assert user in users def test_delete_user(gl, wait_for_sidekiq): new_user = gl.users.create( { "email": "delete-user@test.com", "username": "delete-user", "name": "delete-user", "password": "delete-user-pass", } ) new_user.delete() result = wait_for_sidekiq(timeout=60) assert result is True, "sidekiq process should have terminated but did not" assert new_user.id not in [user.id for user in gl.users.list()] def test_user_projects_list(gl, user): projects = user.projects.list() assert isinstance(projects, list) assert not projects def test_user_events_list(gl, user): events = user.events.list() assert isinstance(events, list) assert not events def test_user_bio(gl, user): user.bio = "This is the user bio" user.save() def test_list_multiple_users(gl, user): second_email = f"{user.email}.2" second_username = f"{user.username}_2" second_user = gl.users.create( { "email": second_email, "username": second_username, "name": "Foo Bar", "password": "foobar_password", } ) assert gl.users.list(search=second_user.username)[0].id == second_user.id expected = [user, second_user] actual = list(gl.users.list(search=user.username)) assert len(expected) == len(actual) assert len(gl.users.list(search="asdf")) == 0 def test_user_gpg_keys(gl, user, GPG_KEY): gkey = user.gpgkeys.create({"key": GPG_KEY}) assert len(user.gpgkeys.list()) == 1 # Seems broken on the gitlab side # gkey = user.gpgkeys.get(gkey.id) gkey.delete() assert len(user.gpgkeys.list()) == 0 def test_user_ssh_keys(gl, user, SSH_KEY): key = user.keys.create({"title": "testkey", "key": SSH_KEY}) assert len(user.keys.list()) == 1 key.delete() assert len(user.keys.list()) == 0 def test_user_email(gl, user): email = user.emails.create({"email": "foo2@bar.com"}) assert len(user.emails.list()) == 1 email.delete() assert len(user.emails.list()) == 0 def test_user_custom_attributes(gl, user): attrs = user.customattributes.list() assert len(attrs) == 0 attr = 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(user.customattributes.list()) == 1 attr = user.customattributes.set("key", "value2") attr = user.customattributes.get("key") assert attr.value == "value2" assert len(user.customattributes.list()) == 1 attr.delete() assert len(user.customattributes.list()) == 0 def test_user_impersonation_tokens(gl, user): token = user.impersonationtokens.create( {"name": "token1", "scopes": ["api", "read_user"]} ) tokens = user.impersonationtokens.list(state="active") assert len(tokens) == 1 token.delete() tokens = user.impersonationtokens.list(state="active") assert len(tokens) == 0 tokens = user.impersonationtokens.list(state="inactive") assert len(tokens) == 1 def test_user_identities(gl, user): provider = "test_provider" user.provider = provider user.extern_uid = "1" user.save() assert provider in [item["provider"] for item in user.identities] user.identityproviders.delete(provider) user = gl.users.get(user.id) assert provider not in [item["provider"] for item in user.identities] python-gitlab-2.10.1/tests/functional/api/test_variables.py000066400000000000000000000026061416141341200240130ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ee/api/instance_level_ci_variables.html https://docs.gitlab.com/ee/api/project_level_variables.html https://docs.gitlab.com/ee/api/group_level_variables.html """ def test_instance_variables(gl): variable = gl.variables.create({"key": "key1", "value": "value1"}) assert variable.value == "value1" assert len(gl.variables.list()) == 1 variable.value = "new_value1" variable.save() variable = gl.variables.get(variable.key) assert variable.value == "new_value1" variable.delete() assert len(gl.variables.list()) == 0 def test_group_variables(group): variable = group.variables.create({"key": "key1", "value": "value1"}) assert variable.value == "value1" assert len(group.variables.list()) == 1 variable.value = "new_value1" variable.save() variable = group.variables.get(variable.key) assert variable.value == "new_value1" variable.delete() assert len(group.variables.list()) == 0 def test_project_variables(project): variable = project.variables.create({"key": "key1", "value": "value1"}) assert variable.value == "value1" assert len(project.variables.list()) == 1 variable.value = "new_value1" variable.save() variable = project.variables.get(variable.key) assert variable.value == "new_value1" variable.delete() assert len(project.variables.list()) == 0 python-gitlab-2.10.1/tests/functional/cli/000077500000000000000000000000001416141341200204245ustar00rootroot00000000000000python-gitlab-2.10.1/tests/functional/cli/__init__.py000066400000000000000000000000001416141341200225230ustar00rootroot00000000000000python-gitlab-2.10.1/tests/functional/cli/conftest.py000066400000000000000000000011371416141341200226250ustar00rootroot00000000000000import pytest @pytest.fixture def gitlab_cli(script_runner, gitlab_config): """Wrapper fixture to help make test cases less verbose.""" def _gitlab_cli(subcommands): """ Return a script_runner.run method that takes a default gitlab command, and subcommands passed as arguments inside test cases. """ command = ["gitlab", "--config-file", gitlab_config] for subcommand in subcommands: # ensure we get strings (e.g from IDs) command.append(str(subcommand)) return script_runner.run(*command) return _gitlab_cli python-gitlab-2.10.1/tests/functional/cli/test_cli_artifacts.py000066400000000000000000000020421416141341200246420ustar00rootroot00000000000000import subprocess import textwrap import time from io import BytesIO from zipfile import is_zipfile content = textwrap.dedent( """\ test-artifact: script: echo "test" > artifact.txt artifacts: untracked: true """ ) data = { "file_path": ".gitlab-ci.yml", "branch": "master", "content": content, "commit_message": "Initial commit", } def test_cli_artifacts(capsysbinary, gitlab_config, gitlab_runner, project): project.files.create(data) jobs = None while not jobs: jobs = project.jobs.list(scope="success") time.sleep(0.5) job = project.jobs.get(jobs[0].id) cmd = [ "gitlab", "--config-file", gitlab_config, "project-job", "artifacts", "--id", str(job.id), "--project-id", str(project.id), ] with capsysbinary.disabled(): artifacts = subprocess.check_output(cmd) assert isinstance(artifacts, bytes) artifacts_zip = BytesIO(artifacts) assert is_zipfile(artifacts_zip) python-gitlab-2.10.1/tests/functional/cli/test_cli_packages.py000066400000000000000000000024501416141341200244430ustar00rootroot00000000000000package_name = "hello-world" package_version = "v1.0.0" file_name = "hello.tar.gz" file_content = "package content" def test_list_project_packages(gitlab_cli, project): cmd = ["project-package", "list", "--project-id", project.id] ret = gitlab_cli(cmd) assert ret.success def test_list_group_packages(gitlab_cli, group): cmd = ["group-package", "list", "--group-id", group.id] ret = gitlab_cli(cmd) assert ret.success def test_upload_generic_package(tmp_path, gitlab_cli, project): path = tmp_path / file_name path.write_text(file_content) cmd = [ "-v", "generic-package", "upload", "--project-id", project.id, "--package-name", package_name, "--path", path, "--package-version", package_version, "--file-name", file_name, ] ret = gitlab_cli(cmd) assert "201 Created" in ret.stdout def test_download_generic_package(gitlab_cli, project): cmd = [ "generic-package", "download", "--project-id", project.id, "--package-name", package_name, "--package-version", package_version, "--file-name", file_name, ] ret = gitlab_cli(cmd) assert ret.stdout == file_content python-gitlab-2.10.1/tests/functional/cli/test_cli_v4.py000066400000000000000000000347531416141341200232310ustar00rootroot00000000000000import os import time def test_create_project(gitlab_cli): name = "test-project1" cmd = ["project", "create", "--name", name] ret = gitlab_cli(cmd) assert ret.success assert name in ret.stdout def test_update_project(gitlab_cli, project): description = "My New Description" cmd = ["project", "update", "--id", project.id, "--description", description] ret = gitlab_cli(cmd) assert ret.success assert description in ret.stdout def test_create_group(gitlab_cli): name = "test-group1" path = "group1" cmd = ["group", "create", "--name", name, "--path", path] ret = gitlab_cli(cmd) assert ret.success assert name in ret.stdout assert path in ret.stdout def test_update_group(gitlab_cli, gl, group): description = "My New Description" cmd = ["group", "update", "--id", group.id, "--description", description] ret = gitlab_cli(cmd) assert ret.success group = gl.groups.get(group.id) assert group.description == description def test_create_user(gitlab_cli, gl): email = "fake@email.com" username = "user1" name = "User One" password = "fakepassword" cmd = [ "user", "create", "--email", email, "--username", username, "--name", name, "--password", password, ] ret = gitlab_cli(cmd) assert ret.success user = gl.users.list(username=username)[0] assert user.email == email assert user.username == username assert user.name == name def test_get_user_by_id(gitlab_cli, user): cmd = ["user", "get", "--id", user.id] ret = gitlab_cli(cmd) assert ret.success assert str(user.id) in ret.stdout def test_list_users_verbose_output(gitlab_cli): cmd = ["-v", "user", "list"] ret = gitlab_cli(cmd) assert ret.success assert "avatar-url" in ret.stdout def test_cli_args_not_in_output(gitlab_cli): cmd = ["-v", "user", "list"] ret = gitlab_cli(cmd) assert "config-file" not in ret.stdout def test_add_member_to_project(gitlab_cli, project, user): access_level = "40" cmd = [ "project-member", "create", "--project-id", project.id, "--user-id", user.id, "--access-level", access_level, ] ret = gitlab_cli(cmd) assert ret.success def test_list_user_memberships(gitlab_cli, user): cmd = ["user-membership", "list", "--user-id", user.id] ret = gitlab_cli(cmd) assert ret.success def test_project_create_file(gitlab_cli, project): file_path = "README" branch = "master" content = "CONTENT" commit_message = "Initial commit" cmd = [ "project-file", "create", "--project-id", project.id, "--file-path", file_path, "--branch", branch, "--content", content, "--commit-message", commit_message, ] ret = gitlab_cli(cmd) assert ret.success def test_create_project_issue(gitlab_cli, project): title = "my issue" description = "my issue description" cmd = [ "project-issue", "create", "--project-id", project.id, "--title", title, "--description", description, ] ret = gitlab_cli(cmd) assert ret.success assert title in ret.stdout def test_create_issue_note(gitlab_cli, issue): body = "body" cmd = [ "project-issue-note", "create", "--project-id", issue.project_id, "--issue-iid", issue.iid, "--body", body, ] ret = gitlab_cli(cmd) assert ret.success def test_create_branch(gitlab_cli, project): branch = "branch1" cmd = [ "project-branch", "create", "--project-id", project.id, "--branch", branch, "--ref", "master", ] ret = gitlab_cli(cmd) assert ret.success def test_create_merge_request(gitlab_cli, project): branch = "branch1" cmd = [ "project-merge-request", "create", "--project-id", project.id, "--source-branch", branch, "--target-branch", "master", "--title", "Update README", ] ret = gitlab_cli(cmd) assert ret.success def test_accept_request_merge(gitlab_cli, project): # MR needs at least 1 commit before we can merge mr = project.mergerequests.list()[0] file_data = { "branch": mr.source_branch, "file_path": "README2", "content": "Content", "commit_message": "Pre-merge commit", } project.files.create(file_data) time.sleep(2) cmd = [ "project-merge-request", "merge", "--project-id", project.id, "--iid", mr.iid, ] ret = gitlab_cli(cmd) assert ret.success def test_revert_commit(gitlab_cli, project): commit = project.commits.list()[0] cmd = [ "project-commit", "revert", "--project-id", project.id, "--id", commit.id, "--branch", "master", ] ret = gitlab_cli(cmd) assert ret.success def test_get_commit_signature_not_found(gitlab_cli, project): commit = project.commits.list()[0] cmd = ["project-commit", "signature", "--project-id", project.id, "--id", commit.id] ret = gitlab_cli(cmd) assert not ret.success assert "404 Signature Not Found" in ret.stderr def test_create_project_label(gitlab_cli, project): name = "prjlabel1" description = "prjlabel1 description" color = "#112233" cmd = [ "-v", "project-label", "create", "--project-id", project.id, "--name", name, "--description", description, "--color", color, ] ret = gitlab_cli(cmd) assert ret.success def test_list_project_labels(gitlab_cli, project): cmd = ["-v", "project-label", "list", "--project-id", project.id] ret = gitlab_cli(cmd) assert ret.success def test_update_project_label(gitlab_cli, label): new_label = "prjlabel2" new_description = "prjlabel2 description" new_color = "#332211" cmd = [ "-v", "project-label", "update", "--project-id", label.project_id, "--name", label.name, "--new-name", new_label, "--description", new_description, "--color", new_color, ] ret = gitlab_cli(cmd) assert ret.success def test_delete_project_label(gitlab_cli, label): # TODO: due to update above, we'd need a function-scope label fixture label_name = "prjlabel2" cmd = [ "-v", "project-label", "delete", "--project-id", label.project_id, "--name", label_name, ] ret = gitlab_cli(cmd) assert ret.success def test_create_group_label(gitlab_cli, group): name = "grouplabel1" description = "grouplabel1 description" color = "#112233" cmd = [ "-v", "group-label", "create", "--group-id", group.id, "--name", name, "--description", description, "--color", color, ] ret = gitlab_cli(cmd) assert ret.success def test_list_group_labels(gitlab_cli, group): cmd = ["-v", "group-label", "list", "--group-id", group.id] ret = gitlab_cli(cmd) assert ret.success def test_update_group_label(gitlab_cli, group_label): new_label = "grouplabel2" new_description = "grouplabel2 description" new_color = "#332211" cmd = [ "-v", "group-label", "update", "--group-id", group_label.group_id, "--name", group_label.name, "--new-name", new_label, "--description", new_description, "--color", new_color, ] ret = gitlab_cli(cmd) assert ret.success def test_delete_group_label(gitlab_cli, group_label): # TODO: due to update above, we'd need a function-scope label fixture new_label = "grouplabel2" cmd = [ "-v", "group-label", "delete", "--group-id", group_label.group_id, "--name", new_label, ] ret = gitlab_cli(cmd) assert ret.success def test_create_project_variable(gitlab_cli, project): key = "junk" value = "car" cmd = [ "-v", "project-variable", "create", "--project-id", project.id, "--key", key, "--value", value, ] ret = gitlab_cli(cmd) assert ret.success def test_get_project_variable(gitlab_cli, variable): cmd = [ "-v", "project-variable", "get", "--project-id", variable.project_id, "--key", variable.key, ] ret = gitlab_cli(cmd) assert ret.success def test_update_project_variable(gitlab_cli, variable): new_value = "bus" cmd = [ "-v", "project-variable", "update", "--project-id", variable.project_id, "--key", variable.key, "--value", new_value, ] ret = gitlab_cli(cmd) assert ret.success def test_list_project_variables(gitlab_cli, project): cmd = ["-v", "project-variable", "list", "--project-id", project.id] ret = gitlab_cli(cmd) assert ret.success def test_delete_project_variable(gitlab_cli, variable): cmd = [ "-v", "project-variable", "delete", "--project-id", variable.project_id, "--key", variable.key, ] ret = gitlab_cli(cmd) assert ret.success def test_delete_branch(gitlab_cli, project): # TODO: branch fixture branch = "branch1" cmd = ["project-branch", "delete", "--project-id", project.id, "--name", branch] ret = gitlab_cli(cmd) assert ret.success def test_project_upload_file(gitlab_cli, project): cmd = [ "project", "upload", "--id", project.id, "--filename", __file__, "--filepath", os.path.realpath(__file__), ] ret = gitlab_cli(cmd) assert ret.success def test_get_application_settings(gitlab_cli): cmd = ["application-settings", "get"] ret = gitlab_cli(cmd) assert ret.success def test_update_application_settings(gitlab_cli): cmd = ["application-settings", "update", "--signup-enabled", "false"] ret = gitlab_cli(cmd) assert ret.success def test_create_project_with_values_from_file(gitlab_cli, tmpdir): name = "gitlab-project-from-file" description = "Multiline\n\nData\n" from_file = tmpdir.join(name) from_file.write(description) from_file_path = f"@{str(from_file)}" cmd = [ "-v", "project", "create", "--name", name, "--description", from_file_path, ] ret = gitlab_cli(cmd) assert ret.success assert description in ret.stdout def test_create_project_deploy_token(gitlab_cli, project): name = "project-token" username = "root" expires_at = "2021-09-09" scopes = "read_registry" cmd = [ "-v", "project-deploy-token", "create", "--project-id", project.id, "--name", name, "--username", username, "--expires-at", expires_at, "--scopes", scopes, ] ret = gitlab_cli(cmd) assert ret.success assert name in ret.stdout assert username in ret.stdout assert expires_at in ret.stdout assert scopes in ret.stdout def test_list_all_deploy_tokens(gitlab_cli, deploy_token): cmd = ["-v", "deploy-token", "list"] ret = gitlab_cli(cmd) assert ret.success assert deploy_token.name in ret.stdout assert str(deploy_token.id) in ret.stdout assert deploy_token.username in ret.stdout assert deploy_token.expires_at in ret.stdout assert deploy_token.scopes[0] in ret.stdout def test_list_project_deploy_tokens(gitlab_cli, deploy_token): cmd = [ "-v", "project-deploy-token", "list", "--project-id", deploy_token.project_id, ] ret = gitlab_cli(cmd) assert ret.success assert deploy_token.name in ret.stdout assert str(deploy_token.id) in ret.stdout assert deploy_token.username in ret.stdout assert deploy_token.expires_at in ret.stdout assert deploy_token.scopes[0] in ret.stdout def test_delete_project_deploy_token(gitlab_cli, deploy_token): cmd = [ "-v", "project-deploy-token", "delete", "--project-id", deploy_token.project_id, "--id", deploy_token.id, ] ret = gitlab_cli(cmd) assert ret.success # TODO assert not in list def test_create_group_deploy_token(gitlab_cli, group): name = "group-token" username = "root" expires_at = "2021-09-09" scopes = "read_registry" cmd = [ "-v", "group-deploy-token", "create", "--group-id", group.id, "--name", name, "--username", username, "--expires-at", expires_at, "--scopes", scopes, ] ret = gitlab_cli(cmd) assert ret.success assert name in ret.stdout assert username in ret.stdout assert expires_at in ret.stdout assert scopes in ret.stdout def test_list_group_deploy_tokens(gitlab_cli, group_deploy_token): cmd = [ "-v", "group-deploy-token", "list", "--group-id", group_deploy_token.group_id, ] ret = gitlab_cli(cmd) assert ret.success assert group_deploy_token.name in ret.stdout assert str(group_deploy_token.id) in ret.stdout assert group_deploy_token.username in ret.stdout assert group_deploy_token.expires_at in ret.stdout assert group_deploy_token.scopes[0] in ret.stdout def test_delete_group_deploy_token(gitlab_cli, group_deploy_token): cmd = [ "-v", "group-deploy-token", "delete", "--group-id", group_deploy_token.group_id, "--id", group_deploy_token.id, ] ret = gitlab_cli(cmd) assert ret.success # TODO assert not in list def test_delete_project(gitlab_cli, project): cmd = ["project", "delete", "--id", project.id] ret = gitlab_cli(cmd) assert ret.success def test_delete_group(gitlab_cli, group): cmd = ["group", "delete", "--id", group.id] ret = gitlab_cli(cmd) assert ret.success python-gitlab-2.10.1/tests/functional/cli/test_cli_variables.py000066400000000000000000000007231416141341200246360ustar00rootroot00000000000000def test_list_instance_variables(gitlab_cli, gl): cmd = ["variable", "list"] ret = gitlab_cli(cmd) assert ret.success def test_list_group_variables(gitlab_cli, group): cmd = ["group-variable", "list", "--group-id", group.id] ret = gitlab_cli(cmd) assert ret.success def test_list_project_variables(gitlab_cli, project): cmd = ["project-variable", "list", "--project-id", project.id] ret = gitlab_cli(cmd) assert ret.success python-gitlab-2.10.1/tests/functional/conftest.py000066400000000000000000000336771416141341200220740ustar00rootroot00000000000000import tempfile import time import uuid from pathlib import Path from subprocess import check_output import pytest import gitlab def reset_gitlab(gl): # previously tools/reset_gitlab.py for project in gl.projects.list(): for deploy_token in project.deploytokens.list(): deploy_token.delete() project.delete() for group in gl.groups.list(): for deploy_token in group.deploytokens.list(): deploy_token.delete() group.delete() for variable in gl.variables.list(): variable.delete() for user in gl.users.list(): if user.username != "root": user.delete(hard_delete=True) def set_token(container, rootdir): set_token_rb = rootdir / "fixtures" / "set_token.rb" with open(set_token_rb, "r") as f: set_token_command = f.read().strip() rails_command = [ "docker", "exec", container, "gitlab-rails", "runner", set_token_command, ] output = check_output(rails_command).decode().strip() return output def pytest_report_collectionfinish(config, startdir, items): return [ "", "Starting GitLab container.", "Waiting for GitLab to reconfigure.", "This may take a few minutes.", ] def pytest_addoption(parser): parser.addoption( "--keep-containers", action="store_true", help="Keep containers running after testing", ) @pytest.fixture(scope="session") def temp_dir(): return Path(tempfile.gettempdir()) @pytest.fixture(scope="session") def test_dir(pytestconfig): return pytestconfig.rootdir / "tests" / "functional" @pytest.fixture(scope="session") def docker_compose_file(test_dir): return test_dir / "fixtures" / "docker-compose.yml" @pytest.fixture(scope="session") def docker_compose_project_name(): """Set a consistent project name to enable optional reuse of containers.""" return "pytest-python-gitlab" @pytest.fixture(scope="session") def docker_cleanup(request): """Conditionally keep containers around by overriding the cleanup command.""" if request.config.getoption("--keep-containers"): # Print version and exit. return "-v" return "down -v" @pytest.fixture(scope="session") def check_is_alive(): """ Return a healthcheck function fixture for the GitLab container spinup. """ def _check(container): logs = ["docker", "logs", container] return "gitlab Reconfigured!" in check_output(logs).decode() return _check @pytest.fixture def wait_for_sidekiq(gl): """ Return a helper function to wait until there are no busy sidekiq processes. Use this with asserts for slow tasks (group/project/user creation/deletion). """ def _wait(timeout=30, step=0.5): for _ in range(timeout): time.sleep(step) busy = False processes = gl.sidekiq.process_metrics()["processes"] for process in processes: if process["busy"]: busy = True if not busy: return True return False return _wait @pytest.fixture(scope="session") def gitlab_config(check_is_alive, docker_ip, docker_services, temp_dir, test_dir): config_file = temp_dir / "python-gitlab.cfg" port = docker_services.port_for("gitlab", 80) docker_services.wait_until_responsive( timeout=200, pause=5, check=lambda: check_is_alive("gitlab-test") ) token = set_token("gitlab-test", rootdir=test_dir) config = f"""[global] default = local timeout = 60 [local] url = http://{docker_ip}:{port} private_token = {token} api_version = 4""" with open(config_file, "w") as f: f.write(config) return config_file @pytest.fixture(scope="session") def gl(gitlab_config): """Helper instance to make fixtures and asserts directly via the API.""" instance = gitlab.Gitlab.from_config("local", [gitlab_config]) reset_gitlab(instance) return instance @pytest.fixture(scope="session") def gitlab_runner(gl): container = "gitlab-runner-test" runner_name = "python-gitlab-runner" token = "registration-token" url = "http://gitlab" docker_exec = ["docker", "exec", container, "gitlab-runner"] register = [ "register", "--run-untagged", "--non-interactive", "--registration-token", token, "--name", runner_name, "--url", url, "--clone-url", url, "--executor", "shell", ] unregister = ["unregister", "--name", runner_name] yield check_output(docker_exec + register).decode() check_output(docker_exec + unregister).decode() @pytest.fixture(scope="module") def group(gl): """Group fixture for group API resource tests.""" _id = uuid.uuid4().hex data = { "name": f"test-group-{_id}", "path": f"group-{_id}", } group = gl.groups.create(data) yield group try: group.delete() except gitlab.exceptions.GitlabDeleteError as e: print(f"Group already deleted: {e}") @pytest.fixture(scope="module") def project(gl): """Project fixture for project API resource tests.""" _id = uuid.uuid4().hex name = f"test-project-{_id}" project = gl.projects.create(name=name) yield project try: project.delete() except gitlab.exceptions.GitlabDeleteError as e: print(f"Project already deleted: {e}") @pytest.fixture(scope="function") def merge_request(project, wait_for_sidekiq): """Fixture used to create a merge_request. It will create a branch, add a commit to the branch, and then create a merge request against project.default_branch. The MR will be returned. When finished any created merge requests and branches will be deleted. NOTE: No attempt is made to restore project.default_branch to its previous state. So if the merge request is merged then its content will be in the project.default_branch branch. """ to_delete = [] def _merge_request(*, source_branch: str): # Wait for processes to be done before we start... # NOTE(jlvillal): Sometimes the CI would give a "500 Internal Server # Error". Hoping that waiting until all other processes are done will # help with that. result = wait_for_sidekiq(timeout=60) assert result is True, "sidekiq process should have terminated but did not" project.refresh() # Gets us the current default branch project.branches.create( {"branch": source_branch, "ref": project.default_branch} ) # NOTE(jlvillal): Must create a commit in the new branch before we can # create an MR that will work. project.files.create( { "file_path": f"README.{source_branch}", "branch": source_branch, "content": "Initial content", "commit_message": "New commit in new branch", } ) mr = project.mergerequests.create( { "source_branch": source_branch, "target_branch": project.default_branch, "title": "Should remove source branch", "remove_source_branch": True, } ) result = wait_for_sidekiq(timeout=60) assert result is True, "sidekiq process should have terminated but did not" mr_iid = mr.iid for _ in range(60): mr = project.mergerequests.get(mr_iid) if mr.merge_status != "checking": break time.sleep(0.5) assert mr.merge_status != "checking" to_delete.append((mr.iid, source_branch)) return mr yield _merge_request for mr_iid, source_branch in to_delete: project.mergerequests.delete(mr_iid) try: project.branches.delete(source_branch) except gitlab.exceptions.GitlabDeleteError: # Ignore if branch was already deleted pass @pytest.fixture(scope="module") def project_file(project): """File fixture for tests requiring a project with files and branches.""" project_file = project.files.create( { "file_path": "README", "branch": "master", "content": "Initial content", "commit_message": "Initial commit", } ) return project_file @pytest.fixture(scope="function") def release(project, project_file): _id = uuid.uuid4().hex name = f"test-release-{_id}" project.refresh() # Gets us the current default branch release = project.releases.create( { "name": name, "tag_name": _id, "description": "description", "ref": project.default_branch, } ) return release @pytest.fixture(scope="module") def user(gl): """User fixture for user API resource tests.""" _id = uuid.uuid4().hex email = f"user{_id}@email.com" username = f"user{_id}" name = f"User {_id}" password = "fakepassword" user = gl.users.create(email=email, username=username, name=name, password=password) yield user try: user.delete() except gitlab.exceptions.GitlabDeleteError as e: print(f"User already deleted: {e}") @pytest.fixture(scope="module") def issue(project): """Issue fixture for issue API resource tests.""" _id = uuid.uuid4().hex data = {"title": f"Issue {_id}", "description": f"Issue {_id} description"} return project.issues.create(data) @pytest.fixture(scope="module") def milestone(project): _id = uuid.uuid4().hex data = {"title": f"milestone{_id}"} return project.milestones.create(data) @pytest.fixture(scope="module") def label(project): """Label fixture for project label API resource tests.""" _id = uuid.uuid4().hex data = { "name": f"prjlabel{_id}", "description": f"prjlabel1 {_id} description", "color": "#112233", } return project.labels.create(data) @pytest.fixture(scope="module") def group_label(group): """Label fixture for group label API resource tests.""" _id = uuid.uuid4().hex data = { "name": f"grplabel{_id}", "description": f"grplabel1 {_id} description", "color": "#112233", } return group.labels.create(data) @pytest.fixture(scope="module") def variable(project): """Variable fixture for project variable API resource tests.""" _id = uuid.uuid4().hex data = {"key": f"var{_id}", "value": f"Variable {_id}"} return project.variables.create(data) @pytest.fixture(scope="module") def deploy_token(project): """Deploy token fixture for project deploy token API resource tests.""" _id = uuid.uuid4().hex data = { "name": f"token-{_id}", "username": "root", "expires_at": "2021-09-09", "scopes": "read_registry", } return project.deploytokens.create(data) @pytest.fixture(scope="module") def group_deploy_token(group): """Deploy token fixture for group deploy token API resource tests.""" _id = uuid.uuid4().hex data = { "name": f"group-token-{_id}", "username": "root", "expires_at": "2021-09-09", "scopes": "read_registry", } return group.deploytokens.create(data) @pytest.fixture(scope="session") def GPG_KEY(): return """-----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-----""" @pytest.fixture(scope="session") def SSH_KEY(): return ( "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDZAjAX8vTiHD7Yi3/EzuVaDChtih" "79HyJZ6H9dEqxFfmGA1YnncE0xujQ64TCebhkYJKzmTJCImSVkOu9C4hZgsw6eE76n" "+Cg3VwEeDUFy+GXlEJWlHaEyc3HWioxgOALbUp3rOezNh+d8BDwwqvENGoePEBsz5l" "a6WP5lTi/HJIjAl6Hu+zHgdj1XVExeH+S52EwpZf/ylTJub0Bl5gHwf/siVE48mLMI" "sqrukXTZ6Zg+8EHAIvIQwJ1dKcXe8P5IoLT7VKrbkgAnolS0I8J+uH7KtErZJb5oZh" "S4OEwsNpaXMAr+6/wWSpircV2/e7sFLlhlKBC4Iq1MpqlZ7G3p foo@bar" ) @pytest.fixture(scope="session") def DEPLOY_KEY(): return ( "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFdRyjJQh+1niBpXqE2I8dzjG" "MXFHlRjX9yk/UfOn075IdaockdU58sw2Ai1XIWFpZpfJkW7z+P47ZNSqm1gzeXI" "rtKa9ZUp8A7SZe8vH4XVn7kh7bwWCUirqtn8El9XdqfkzOs/+FuViriUWoJVpA6" "WZsDNaqINFKIA5fj/q8XQw+BcS92L09QJg9oVUuH0VVwNYbU2M2IRmSpybgC/gu" "uWTrnCDMmLItksATifLvRZwgdI8dr+q6tbxbZknNcgEPrI2jT0hYN9ZcjNeWuyv" "rke9IepE7SPBT41C+YtUX4dfDZDmczM1cE0YL/krdUCfuZHMa4ZS2YyNd6slufc" "vn bar@foo" ) python-gitlab-2.10.1/tests/functional/ee-test.py000077500000000000000000000077201416141341200216060ustar00rootroot00000000000000#!/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, [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, [1], []) approval = mr.approvals.get() assert approval.approvers[0]["user"]["id"] == 1 ars = project1.approvalrules.list(all=True) assert len(ars) == 0 project1.approvalrules.create( {"name": "approval-rule", "approvals_required": 1, "group_ids": [group1.id]} ) ars = project1.approvalrules.list(all=True) assert len(ars) == 1 assert 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 is False pr.delete() end_log() start_log("license") license = gl.get_license() assert "user_limit" in license 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.10.1/tests/functional/fixtures/000077500000000000000000000000001416141341200215265ustar00rootroot00000000000000python-gitlab-2.10.1/tests/functional/fixtures/.env000066400000000000000000000000661416141341200223210ustar00rootroot00000000000000GITLAB_IMAGE=gitlab/gitlab-ce GITLAB_TAG=13.12.0-ce.0 python-gitlab-2.10.1/tests/functional/fixtures/avatar.png000066400000000000000000000011201416141341200235040ustar00rootroot00000000000000PNG  IHDRaIDAT8mn0ECRQ)]UD % 3Ggg<L80 8瘦 !RJ(PJ!@0um4 mvDQncgK:4 땪kC$Iv Z#st]vrPӉ,i(x||$sy}Ǒ먪(x}}# sVZιmϼS5qs^R1ڭc꺦,K3E'Y|>S%4Mq-y~4 u]SUՂJJ:sw~_cֶ)y\% Ð,H$I8($IȲ0 RM4M9m'#i)PJR c Za`&i߀<ϱbY0J)1ppv!Z,cEƘB SJEY1#v}?xrX}uVA@󖀿] > T.]IENDB`python-gitlab-2.10.1/tests/functional/fixtures/docker-compose.yml000066400000000000000000000025501416141341200251650ustar00rootroot00000000000000version: '3' networks: gitlab-network: name: gitlab-network services: gitlab: image: '${GITLAB_IMAGE}:${GITLAB_TAG}' container_name: 'gitlab-test' hostname: 'gitlab.test' privileged: true # Just in case https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/1350 environment: GITLAB_ROOT_PASSWORD: 5iveL!fe GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN: registration-token GITLAB_OMNIBUS_CONFIG: | external_url 'http://gitlab.test' 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 ports: - '8080:80' - '2222:22' networks: - gitlab-network gitlab-runner: image: gitlab/gitlab-runner:latest container_name: 'gitlab-runner-test' depends_on: - gitlab networks: - gitlab-network python-gitlab-2.10.1/tests/functional/fixtures/set_token.rb000066400000000000000000000005021416141341200240430ustar00rootroot00000000000000# https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#programmatically-creating-a-personal-access-token user = User.find_by_username('root') token = user.personal_access_tokens.first_or_create(scopes: [:api, :sudo], name: 'default'); token.set_token('python-gitlab-token'); token.save! puts token.token python-gitlab-2.10.1/tests/unit/000077500000000000000000000000001416141341200164725ustar00rootroot00000000000000python-gitlab-2.10.1/tests/unit/__init__.py000066400000000000000000000000001416141341200205710ustar00rootroot00000000000000python-gitlab-2.10.1/tests/unit/conftest.py000066400000000000000000000024261416141341200206750ustar00rootroot00000000000000import pytest import gitlab @pytest.fixture def gl(): return gitlab.Gitlab( "http://localhost", private_token="private_token", ssl_verify=True, api_version=4, ) # Todo: parametrize, but check what tests it's really useful for @pytest.fixture def gl_trailing(): return gitlab.Gitlab( "http://localhost/", private_token="private_token", api_version=4 ) @pytest.fixture def default_config(tmpdir): valid_config = """[global] default = one ssl_verify = true timeout = 2 [one] url = http://one.url private_token = ABCDEF """ config_path = tmpdir.join("python-gitlab.cfg") config_path.write(valid_config) return str(config_path) @pytest.fixture def tag_name(): return "v1.0.0" @pytest.fixture def group(gl): return gl.groups.get(1, lazy=True) @pytest.fixture def project(gl): return gl.projects.get(1, lazy=True) @pytest.fixture def project_issue(project): return project.issues.get(1, lazy=True) @pytest.fixture def project_merge_request(project): return project.mergerequests.get(1, lazy=True) @pytest.fixture def release(project, tag_name): return project.releases.get(tag_name, lazy=True) @pytest.fixture def user(gl): return gl.users.get(1, lazy=True) python-gitlab-2.10.1/tests/unit/data/000077500000000000000000000000001416141341200174035ustar00rootroot00000000000000python-gitlab-2.10.1/tests/unit/data/todo.json000066400000000000000000000050671416141341200212530ustar00rootroot00000000000000[ { "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.10.1/tests/unit/mixins/000077500000000000000000000000001416141341200200015ustar00rootroot00000000000000python-gitlab-2.10.1/tests/unit/mixins/__init__.py000066400000000000000000000000001416141341200221000ustar00rootroot00000000000000python-gitlab-2.10.1/tests/unit/mixins/test_meta_mixins.py000066400000000000000000000025571416141341200237400ustar00rootroot00000000000000from gitlab.mixins import ( CreateMixin, CRUDMixin, DeleteMixin, GetMixin, ListMixin, NoUpdateMixin, RetrieveMixin, UpdateMixin, ) def test_retrieve_mixin(): class M(RetrieveMixin): pass obj = M() assert hasattr(obj, "list") assert hasattr(obj, "get") assert not hasattr(obj, "create") assert not hasattr(obj, "update") assert not hasattr(obj, "delete") assert isinstance(obj, ListMixin) assert isinstance(obj, GetMixin) def test_crud_mixin(): class M(CRUDMixin): pass obj = M() assert hasattr(obj, "get") assert hasattr(obj, "list") assert hasattr(obj, "create") assert hasattr(obj, "update") assert hasattr(obj, "delete") assert isinstance(obj, ListMixin) assert isinstance(obj, GetMixin) assert isinstance(obj, CreateMixin) assert isinstance(obj, UpdateMixin) assert isinstance(obj, DeleteMixin) def test_no_update_mixin(): class M(NoUpdateMixin): pass obj = M() assert hasattr(obj, "get") assert hasattr(obj, "list") assert hasattr(obj, "create") assert not hasattr(obj, "update") assert hasattr(obj, "delete") assert isinstance(obj, ListMixin) assert isinstance(obj, GetMixin) assert isinstance(obj, CreateMixin) assert not isinstance(obj, UpdateMixin) assert isinstance(obj, DeleteMixin) python-gitlab-2.10.1/tests/unit/mixins/test_mixin_methods.py000066400000000000000000000221671416141341200242710ustar00rootroot00000000000000import pytest from httmock import HTTMock, response, urlmatch # noqa from gitlab import base from gitlab.mixins import ( CreateMixin, DeleteMixin, GetMixin, GetWithoutIdMixin, ListMixin, RefreshMixin, SaveMixin, SetMixin, UpdateMixin, ) class FakeObject(base.RESTObject): pass class FakeManager(base.RESTManager): _path = "/tests" _obj_cls = FakeObject def test_get_mixin(gl): 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(gl) obj = mgr.get(42) assert isinstance(obj, FakeObject) assert obj.foo == "bar" assert obj.id == 42 def test_refresh_mixin(gl): class TestClass(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(gl) obj = TestClass(mgr, {"id": 42}) res = obj.refresh() assert res is None assert obj.foo == "bar" assert obj.id == 42 def test_get_without_id_mixin(gl): 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(gl) obj = mgr.get() assert isinstance(obj, FakeObject) assert obj.foo == "bar" assert not hasattr(obj, "id") def test_list_mixin(gl): 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(gl) obj_list = mgr.list(as_list=False) assert isinstance(obj_list, base.RESTObjectList) for obj in obj_list: assert isinstance(obj, FakeObject) assert obj.id in (42, 43) # test list() obj_list = mgr.list(all=True) assert isinstance(obj_list, list) assert obj_list[0].id == 42 assert obj_list[1].id == 43 assert isinstance(obj_list[0], FakeObject) assert len(obj_list) == 2 def test_list_other_url(gl): 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(gl) obj_list = mgr.list(path="/others", as_list=False) assert isinstance(obj_list, base.RESTObjectList) obj = obj_list.next() assert obj.id == 42 assert obj.foo == "bar" with pytest.raises(StopIteration): obj_list.next() def test_create_mixin_missing_attrs(gl): class M(CreateMixin, FakeManager): _create_attrs = base.RequiredOptional( required=("foo",), optional=("bar", "baz") ) mgr = M(gl) data = {"foo": "bar", "baz": "blah"} mgr._check_missing_create_attrs(data) data = {"baz": "blah"} with pytest.raises(AttributeError) as error: mgr._check_missing_create_attrs(data) assert "foo" in str(error.value) def test_create_mixin(gl): class M(CreateMixin, FakeManager): _create_attrs = base.RequiredOptional( required=("foo",), optional=("bar", "baz") ) _update_attrs = base.RequiredOptional(required=("foo",), optional=("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(gl) obj = mgr.create({"foo": "bar"}) assert isinstance(obj, FakeObject) assert obj.id == 42 assert obj.foo == "bar" def test_create_mixin_custom_path(gl): class M(CreateMixin, FakeManager): _create_attrs = base.RequiredOptional( required=("foo",), optional=("bar", "baz") ) _update_attrs = base.RequiredOptional(required=("foo",), optional=("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(gl) obj = mgr.create({"foo": "bar"}, path="/others") assert isinstance(obj, FakeObject) assert obj.id == 42 assert obj.foo == "bar" def test_update_mixin_missing_attrs(gl): class M(UpdateMixin, FakeManager): _update_attrs = base.RequiredOptional( required=("foo",), optional=("bar", "baz") ) mgr = M(gl) data = {"foo": "bar", "baz": "blah"} mgr._check_missing_update_attrs(data) data = {"baz": "blah"} with pytest.raises(AttributeError) as error: mgr._check_missing_update_attrs(data) assert "foo" in str(error.value) def test_update_mixin(gl): class M(UpdateMixin, FakeManager): _create_attrs = base.RequiredOptional( required=("foo",), optional=("bar", "baz") ) _update_attrs = base.RequiredOptional(required=("foo",), optional=("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(gl) server_data = mgr.update(42, {"foo": "baz"}) assert isinstance(server_data, dict) assert server_data["id"] == 42 assert server_data["foo"] == "baz" def test_update_mixin_no_id(gl): class M(UpdateMixin, FakeManager): _create_attrs = base.RequiredOptional( required=("foo",), optional=("bar", "baz") ) _update_attrs = base.RequiredOptional(required=("foo",), optional=("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(gl) server_data = mgr.update(new_data={"foo": "baz"}) assert isinstance(server_data, dict) assert server_data["foo"] == "baz" def test_delete_mixin(gl): 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(gl) mgr.delete(42) def test_save_mixin(gl): class M(UpdateMixin, FakeManager): pass class TestClass(SaveMixin, base.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(gl) obj = TestClass(mgr, {"id": 42, "foo": "bar"}) obj.foo = "baz" obj.save() assert obj._attrs["foo"] == "baz" assert obj._updated_attrs == {} def test_set_mixin(gl): 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(gl) obj = mgr.set("foo", "bar") assert isinstance(obj, FakeObject) assert obj.key == "foo" assert obj.value == "bar" python-gitlab-2.10.1/tests/unit/mixins/test_object_mixins_attributes.py000066400000000000000000000037421416141341200265230ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Copyright (C) 2014 Mika Mäenpää , # Tampere University of Technology # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . from gitlab.mixins import ( AccessRequestMixin, SetMixin, SubscribableMixin, TimeTrackingMixin, TodoMixin, UserAgentDetailMixin, ) def test_access_request_mixin(): class TestClass(AccessRequestMixin): pass obj = TestClass() assert hasattr(obj, "approve") def test_subscribable_mixin(): class TestClass(SubscribableMixin): pass obj = TestClass() assert hasattr(obj, "subscribe") assert hasattr(obj, "unsubscribe") def test_todo_mixin(): class TestClass(TodoMixin): pass obj = TestClass() assert hasattr(obj, "todo") def test_time_tracking_mixin(): class TestClass(TimeTrackingMixin): pass obj = TestClass() assert hasattr(obj, "time_stats") assert hasattr(obj, "time_estimate") assert hasattr(obj, "reset_time_estimate") assert hasattr(obj, "add_spent_time") assert hasattr(obj, "reset_spent_time") def test_set_mixin(): class TestClass(SetMixin): pass obj = TestClass() assert hasattr(obj, "set") def test_user_agent_detail_mixin(): class TestClass(UserAgentDetailMixin): pass obj = TestClass() assert hasattr(obj, "user_agent_detail") python-gitlab-2.10.1/tests/unit/objects/000077500000000000000000000000001416141341200201235ustar00rootroot00000000000000python-gitlab-2.10.1/tests/unit/objects/__init__.py000066400000000000000000000000001416141341200222220ustar00rootroot00000000000000python-gitlab-2.10.1/tests/unit/objects/conftest.py000066400000000000000000000036661416141341200223350ustar00rootroot00000000000000"""Common mocks for resources in gitlab.v4.objects""" import re import pytest import responses @pytest.fixture def binary_content(): return b"binary content" @pytest.fixture def accepted_content(): return {"message": "202 Accepted"} @pytest.fixture def created_content(): return {"message": "201 Created"} @pytest.fixture def no_content(): return {"message": "204 No Content"} @pytest.fixture def resp_export(accepted_content, binary_content): """Common fixture for group and project exports.""" export_status_content = { "id": 1, "description": "Itaque perspiciatis minima aspernatur", "name": "Gitlab Test", "name_with_namespace": "Gitlab Org / Gitlab Test", "path": "gitlab-test", "path_with_namespace": "gitlab-org/gitlab-test", "created_at": "2017-08-29T04:36:44.383Z", "export_status": "finished", "_links": { "api_url": "https://gitlab.test/api/v4/projects/1/export/download", "web_url": "https://gitlab.test/gitlab-test/download_export", }, } with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: rsps.add( method=responses.POST, url=re.compile(r".*/api/v4/(groups|projects)/1/export"), json=accepted_content, content_type="application/json", status=202, ) rsps.add( method=responses.GET, url=re.compile(r".*/api/v4/(groups|projects)/1/export/download"), body=binary_content, content_type="application/octet-stream", status=200, ) # Currently only project export supports status checks rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/export", json=export_status_content, content_type="application/json", status=200, ) yield rsps python-gitlab-2.10.1/tests/unit/objects/test_appearance.py000066400000000000000000000040171416141341200236350ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ce/api/appearance.html """ import pytest import responses title = "GitLab Test Instance" description = "gitlab-test.example.com" new_title = "new-title" new_description = "new-description" @pytest.fixture def resp_application_appearance(): content = { "title": title, "description": description, "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, } with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/application/appearance", json=content, content_type="application/json", status=200, ) updated_content = dict(content) updated_content["title"] = new_title updated_content["description"] = new_description rsps.add( method=responses.PUT, url="http://localhost/api/v4/application/appearance", json=updated_content, content_type="application/json", status=200, ) yield rsps def test_get_update_appearance(gl, resp_application_appearance): appearance = gl.appearance.get() assert appearance.title == title assert appearance.description == description appearance.title = new_title appearance.description = new_description appearance.save() assert appearance.title == new_title assert appearance.description == new_description def test_update_appearance(gl, resp_application_appearance): gl.appearance.update(title=new_title, description=new_description) python-gitlab-2.10.1/tests/unit/objects/test_applications.py000066400000000000000000000021611416141341200242220ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ce/api/applications.html """ import pytest import responses title = "GitLab Test Instance" description = "gitlab-test.example.com" new_title = "new-title" new_description = "new-description" @pytest.fixture def resp_application_create(): content = { "name": "test_app", "redirect_uri": "http://localhost:8080", "scopes": ["api", "email"], } with responses.RequestsMock() as rsps: rsps.add( method=responses.POST, url="http://localhost/api/v4/applications", json=content, content_type="application/json", status=200, ) yield rsps def test_create_application(gl, resp_application_create): application = gl.applications.create( { "name": "test_app", "redirect_uri": "http://localhost:8080", "scopes": ["api", "email"], "confidential": False, } ) assert application.name == "test_app" assert application.redirect_uri == "http://localhost:8080" assert application.scopes == ["api", "email"] python-gitlab-2.10.1/tests/unit/objects/test_audit_events.py000066400000000000000000000055601416141341200242340ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ee/api/audit_events.html#project-audit-events """ import re import pytest import responses from gitlab.v4.objects.audit_events import ( AuditEvent, GroupAuditEvent, ProjectAuditEvent, ) id = 5 audit_events_content = { "id": 5, "author_id": 1, "entity_id": 7, "entity_type": "Project", "details": { "change": "prevent merge request approval from reviewers", "from": "", "to": "true", "author_name": "Administrator", "target_id": 7, "target_type": "Project", "target_details": "twitter/typeahead-js", "ip_address": "127.0.0.1", "entity_path": "twitter/typeahead-js", }, "created_at": "2020-05-26T22:55:04.230Z", } audit_events_url = re.compile( r"http://localhost/api/v4/((groups|projects)/1/)?audit_events" ) audit_events_url_id = re.compile( rf"http://localhost/api/v4/((groups|projects)/1/)?audit_events/{id}" ) @pytest.fixture def resp_list_audit_events(): with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url=audit_events_url, json=[audit_events_content], content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_get_audit_event(): with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url=audit_events_url_id, json=audit_events_content, content_type="application/json", status=200, ) yield rsps def test_list_instance_audit_events(gl, resp_list_audit_events): audit_events = gl.audit_events.list() assert isinstance(audit_events, list) assert isinstance(audit_events[0], AuditEvent) assert audit_events[0].id == id def test_get_instance_audit_events(gl, resp_get_audit_event): audit_event = gl.audit_events.get(id) assert isinstance(audit_event, AuditEvent) assert audit_event.id == id def test_list_group_audit_events(group, resp_list_audit_events): audit_events = group.audit_events.list() assert isinstance(audit_events, list) assert isinstance(audit_events[0], GroupAuditEvent) assert audit_events[0].id == id def test_get_group_audit_events(group, resp_get_audit_event): audit_event = group.audit_events.get(id) assert isinstance(audit_event, GroupAuditEvent) assert audit_event.id == id def test_list_project_audit_events(project, resp_list_audit_events): audit_events = project.audit_events.list() assert isinstance(audit_events, list) assert isinstance(audit_events[0], ProjectAuditEvent) assert audit_events[0].id == id def test_get_project_audit_events(project, resp_get_audit_event): audit_event = project.audit_events.get(id) assert isinstance(audit_event, ProjectAuditEvent) assert audit_event.id == id python-gitlab-2.10.1/tests/unit/objects/test_badges.py000066400000000000000000000132631416141341200227660ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ee/api/project_badges.html GitLab API: https://docs.gitlab.com/ee/api/group_badges.html """ import re import pytest import responses from gitlab.v4.objects import GroupBadge, ProjectBadge link_url = ( "http://example.com/ci_status.svg?project=example-org/example-project&ref=master" ) image_url = "https://example.io/my/badge" rendered_link_url = ( "http://example.com/ci_status.svg?project=example-org/example-project&ref=master" ) rendered_image_url = "https://example.io/my/badge" new_badge = { "link_url": link_url, "image_url": image_url, } badge_content = { "name": "Coverage", "id": 1, "link_url": link_url, "image_url": image_url, "rendered_link_url": rendered_image_url, "rendered_image_url": rendered_image_url, } preview_badge_content = { "link_url": link_url, "image_url": image_url, "rendered_link_url": rendered_link_url, "rendered_image_url": rendered_image_url, } @pytest.fixture() def resp_get_badge(): with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url=re.compile(r"http://localhost/api/v4/(projects|groups)/1/badges/1"), json=badge_content, content_type="application/json", status=200, ) yield rsps @pytest.fixture() def resp_list_badges(): with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url=re.compile(r"http://localhost/api/v4/(projects|groups)/1/badges"), json=[badge_content], content_type="application/json", status=200, ) yield rsps @pytest.fixture() def resp_create_badge(): with responses.RequestsMock() as rsps: rsps.add( method=responses.POST, url=re.compile(r"http://localhost/api/v4/(projects|groups)/1/badges"), json=badge_content, content_type="application/json", status=200, ) yield rsps @pytest.fixture() def resp_update_badge(): updated_content = dict(badge_content) updated_content["link_url"] = "http://link_url" with responses.RequestsMock() as rsps: rsps.add( method=responses.PUT, url=re.compile(r"http://localhost/api/v4/(projects|groups)/1/badges/1"), json=updated_content, content_type="application/json", status=200, ) yield rsps @pytest.fixture() def resp_delete_badge(no_content): with responses.RequestsMock() as rsps: rsps.add( method=responses.DELETE, url=re.compile(r"http://localhost/api/v4/(projects|groups)/1/badges/1"), json=no_content, content_type="application/json", status=204, ) yield rsps @pytest.fixture() def resp_preview_badge(): with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url=re.compile( r"http://localhost/api/v4/(projects|groups)/1/badges/render" ), json=preview_badge_content, content_type="application/json", status=200, ) yield rsps def test_list_project_badges(project, resp_list_badges): badges = project.badges.list() assert isinstance(badges, list) assert isinstance(badges[0], ProjectBadge) def test_list_group_badges(group, resp_list_badges): badges = group.badges.list() assert isinstance(badges, list) assert isinstance(badges[0], GroupBadge) def test_get_project_badge(project, resp_get_badge): badge = project.badges.get(1) assert isinstance(badge, ProjectBadge) assert badge.name == "Coverage" assert badge.id == 1 def test_get_group_badge(group, resp_get_badge): badge = group.badges.get(1) assert isinstance(badge, GroupBadge) assert badge.name == "Coverage" assert badge.id == 1 def test_delete_project_badge(project, resp_delete_badge): badge = project.badges.get(1, lazy=True) badge.delete() def test_delete_group_badge(group, resp_delete_badge): badge = group.badges.get(1, lazy=True) badge.delete() def test_create_project_badge(project, resp_create_badge): badge = project.badges.create(new_badge) assert isinstance(badge, ProjectBadge) assert badge.image_url == image_url def test_create_group_badge(group, resp_create_badge): badge = group.badges.create(new_badge) assert isinstance(badge, GroupBadge) assert badge.image_url == image_url def test_preview_project_badge(project, resp_preview_badge): output = project.badges.render( link_url=link_url, image_url=image_url, ) assert isinstance(output, dict) assert "rendered_link_url" in output assert "rendered_image_url" in output assert output["link_url"] == output["rendered_link_url"] assert output["image_url"] == output["rendered_image_url"] def test_preview_group_badge(group, resp_preview_badge): output = group.badges.render( link_url=link_url, image_url=image_url, ) assert isinstance(output, dict) assert "rendered_link_url" in output assert "rendered_image_url" in output assert output["link_url"] == output["rendered_link_url"] assert output["image_url"] == output["rendered_image_url"] def test_update_project_badge(project, resp_update_badge): badge = project.badges.get(1, lazy=True) badge.link_url = "http://link_url" badge.save() assert badge.link_url == "http://link_url" def test_update_group_badge(group, resp_update_badge): badge = group.badges.get(1, lazy=True) badge.link_url = "http://link_url" badge.save() assert badge.link_url == "http://link_url" python-gitlab-2.10.1/tests/unit/objects/test_bridges.py000066400000000000000000000071261416141341200231610ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ee/api/jobs.html#list-pipeline-bridges """ import pytest import responses from gitlab.v4.objects import ProjectPipelineBridge @pytest.fixture def resp_list_bridges(): export_bridges_content = { "commit": { "author_email": "admin@example.com", "author_name": "Administrator", "created_at": "2015-12-24T16:51:14.000+01:00", "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd", "message": "Test the CI integration.", "short_id": "0ff3ae19", "title": "Test the CI integration.", }, "allow_failure": False, "created_at": "2015-12-24T15:51:21.802Z", "started_at": "2015-12-24T17:54:27.722Z", "finished_at": "2015-12-24T17:58:27.895Z", "duration": 240, "id": 7, "name": "teaspoon", "pipeline": { "id": 6, "ref": "master", "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd", "status": "pending", "created_at": "2015-12-24T15:50:16.123Z", "updated_at": "2015-12-24T18:00:44.432Z", "web_url": "https://example.com/foo/bar/pipelines/6", }, "ref": "master", "stage": "test", "status": "pending", "tag": False, "web_url": "https://example.com/foo/bar/-/jobs/7", "user": { "id": 1, "name": "Administrator", "username": "root", "state": "active", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "web_url": "http://gitlab.dev/root", "created_at": "2015-12-21T13:14:24.077Z", "public_email": "", "skype": "", "linkedin": "", "twitter": "", "website_url": "", "organization": "", }, "downstream_pipeline": { "id": 5, "sha": "f62a4b2fb89754372a346f24659212eb8da13601", "ref": "master", "status": "pending", "created_at": "2015-12-24T17:54:27.722Z", "updated_at": "2015-12-24T17:58:27.896Z", "web_url": "https://example.com/diaspora/diaspora-client/pipelines/5", }, } export_pipelines_content = [ { "id": 6, "status": "pending", "ref": "new-pipeline", "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a", "web_url": "https://example.com/foo/bar/pipelines/47", "created_at": "2016-08-11T11:28:34.085Z", "updated_at": "2016-08-11T11:32:35.169Z", }, ] with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/pipelines/6/bridges", json=[export_bridges_content], content_type="application/json", status=200, ) rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/pipelines", json=export_pipelines_content, content_type="application/json", status=200, ) yield rsps def test_list_projects_pipelines_bridges(project, resp_list_bridges): pipeline = project.pipelines.list()[0] bridges = pipeline.bridges.list() assert isinstance(bridges, list) assert isinstance(bridges[0], ProjectPipelineBridge) assert bridges[0].downstream_pipeline["id"] == 5 assert ( bridges[0].downstream_pipeline["sha"] == "f62a4b2fb89754372a346f24659212eb8da13601" ) python-gitlab-2.10.1/tests/unit/objects/test_commits.py000066400000000000000000000064251416141341200232160ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ce/api/commits.html """ import pytest import responses @pytest.fixture def resp_create_commit(): content = { "id": "ed899a2f4b50b4370feeea94676502b42383c746", "short_id": "ed899a2f", "title": "Commit message", } with responses.RequestsMock() as rsps: rsps.add( method=responses.POST, url="http://localhost/api/v4/projects/1/repository/commits", json=content, content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_commit(): get_content = { "id": "6b2257eabcec3db1f59dafbd84935e3caea04235", "short_id": "6b2257ea", "title": "Initial commit", } revert_content = { "id": "8b090c1b79a14f2bd9e8a738f717824ff53aebad", "short_id": "8b090c1b", "title": 'Revert "Initial commit"', } with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/repository/commits/6b2257ea", json=get_content, content_type="application/json", status=200, ) rsps.add( method=responses.POST, url="http://localhost/api/v4/projects/1/repository/commits/6b2257ea/revert", json=revert_content, content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_get_commit_gpg_signature(): content = { "gpg_key_id": 1, "gpg_key_primary_keyid": "8254AAB3FBD54AC9", "gpg_key_user_name": "John Doe", "gpg_key_user_email": "johndoe@example.com", "verification_status": "verified", "gpg_key_subkey_id": None, } with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/repository/commits/6b2257ea/signature", json=content, content_type="application/json", status=200, ) yield rsps def test_get_commit(project, resp_commit): commit = project.commits.get("6b2257ea") assert commit.short_id == "6b2257ea" assert commit.title == "Initial commit" def test_create_commit(project, resp_create_commit): data = { "branch": "master", "commit_message": "Commit message", "actions": [ { "action": "create", "file_path": "README", "content": "", } ], } commit = project.commits.create(data) assert commit.short_id == "ed899a2f" assert commit.title == data["commit_message"] def test_revert_commit(project, resp_commit): commit = project.commits.get("6b2257ea", lazy=True) revert_commit = commit.revert(branch="master") assert revert_commit["short_id"] == "8b090c1b" assert revert_commit["title"] == 'Revert "Initial commit"' def test_get_commit_gpg_signature(project, resp_get_commit_gpg_signature): commit = project.commits.get("6b2257ea", lazy=True) signature = commit.signature() assert signature["gpg_key_primary_keyid"] == "8254AAB3FBD54AC9" assert signature["verification_status"] == "verified" python-gitlab-2.10.1/tests/unit/objects/test_deploy_tokens.py000066400000000000000000000024231416141341200244140ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ce/api/deploy_tokens.html """ import pytest import responses from gitlab.v4.objects import ProjectDeployToken create_content = { "id": 1, "name": "test_deploy_token", "username": "custom-user", "expires_at": "2022-01-01T00:00:00.000Z", "token": "jMRvtPNxrn3crTAGukpZ", "scopes": ["read_repository"], } @pytest.fixture def resp_deploy_token_create(): with responses.RequestsMock() as rsps: rsps.add( method=responses.POST, url="http://localhost/api/v4/projects/1/deploy_tokens", json=create_content, content_type="application/json", status=200, ) yield rsps def test_deploy_tokens(gl, resp_deploy_token_create): deploy_token = gl.projects.get(1, lazy=True).deploytokens.create( { "name": "test_deploy_token", "expires_at": "2022-01-01T00:00:00.000Z", "username": "custom-user", "scopes": ["read_repository"], } ) assert isinstance(deploy_token, ProjectDeployToken) assert deploy_token.id == 1 assert deploy_token.expires_at == "2022-01-01T00:00:00.000Z" assert deploy_token.username == "custom-user" assert deploy_token.scopes == ["read_repository"] python-gitlab-2.10.1/tests/unit/objects/test_deployments.py000066400000000000000000000024141416141341200241000ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ce/api/deployments.html """ import pytest import responses @pytest.fixture def resp_deployment(): content = {"id": 42, "status": "success", "ref": "master"} with responses.RequestsMock() as rsps: rsps.add( method=responses.POST, url="http://localhost/api/v4/projects/1/deployments", json=content, content_type="application/json", status=200, ) updated_content = dict(content) updated_content["status"] = "failed" rsps.add( method=responses.PUT, url="http://localhost/api/v4/projects/1/deployments/42", json=updated_content, content_type="application/json", status=200, ) yield rsps def test_deployment(project, resp_deployment): deployment = project.deployments.create( { "environment": "Test", "sha": "1agf4gs", "ref": "master", "tag": False, "status": "created", } ) assert deployment.id == 42 assert deployment.status == "success" assert deployment.ref == "master" deployment.status = "failed" deployment.save() assert deployment.status == "failed" python-gitlab-2.10.1/tests/unit/objects/test_environments.py000066400000000000000000000015431416141341200242660ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ce/api/environments.html """ import pytest import responses from gitlab.v4.objects import ProjectEnvironment @pytest.fixture def resp_get_environment(): content = {"name": "environment_name", "id": 1, "last_deployment": "sometime"} with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/environments/1", json=content, content_type="application/json", status=200, ) yield rsps def test_project_environments(project, resp_get_environment): environment = project.environments.get(1) assert isinstance(environment, ProjectEnvironment) assert environment.id == 1 assert environment.last_deployment == "sometime" assert environment.name == "environment_name" python-gitlab-2.10.1/tests/unit/objects/test_groups.py000066400000000000000000000111301416141341200230470ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ce/api/groups.html """ import re import pytest import responses import gitlab from gitlab.v4.objects import GroupDescendantGroup, GroupSubgroup subgroup_descgroup_content = [ { "id": 2, "name": "Bar Group", "path": "foo/bar", "description": "A subgroup of Foo Group", "visibility": "public", "share_with_group_lock": False, "require_two_factor_authentication": False, "two_factor_grace_period": 48, "project_creation_level": "developer", "auto_devops_enabled": None, "subgroup_creation_level": "owner", "emails_disabled": None, "mentions_disabled": None, "lfs_enabled": True, "default_branch_protection": 2, "avatar_url": "http://gitlab.example.com/uploads/group/avatar/1/bar.jpg", "web_url": "http://gitlab.example.com/groups/foo/bar", "request_access_enabled": False, "full_name": "Bar Group", "full_path": "foo/bar", "file_template_project_id": 1, "parent_id": 123, "created_at": "2020-01-15T12:36:29.590Z", }, ] @pytest.fixture def resp_groups(): content = {"name": "name", "id": 1, "path": "path"} with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/groups/1", json=content, content_type="application/json", status=200, ) rsps.add( method=responses.GET, url="http://localhost/api/v4/groups", json=[content], content_type="application/json", status=200, ) rsps.add( method=responses.POST, url="http://localhost/api/v4/groups", json=content, content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_list_subgroups_descendant_groups(): with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url=re.compile( r"http://localhost/api/v4/groups/1/(subgroups|descendant_groups)" ), json=subgroup_descgroup_content, content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_create_import(accepted_content): with responses.RequestsMock() as rsps: rsps.add( method=responses.POST, url="http://localhost/api/v4/groups/import", json=accepted_content, content_type="application/json", status=202, ) yield rsps def test_get_group(gl, resp_groups): data = gl.groups.get(1) assert isinstance(data, gitlab.v4.objects.Group) assert data.name == "name" assert data.path == "path" assert data.id == 1 def test_create_group(gl, resp_groups): name, path = "name", "path" data = gl.groups.create({"name": name, "path": path}) assert isinstance(data, gitlab.v4.objects.Group) assert data.name == name assert data.path == path def test_create_group_export(group, resp_export): export = group.exports.create() assert export.message == "202 Accepted" def test_list_group_subgroups(group, resp_list_subgroups_descendant_groups): subgroups = group.subgroups.list() assert isinstance(subgroups[0], GroupSubgroup) assert subgroups[0].path == subgroup_descgroup_content[0]["path"] def test_list_group_descendant_groups(group, resp_list_subgroups_descendant_groups): descendant_groups = group.descendant_groups.list() assert isinstance(descendant_groups[0], GroupDescendantGroup) assert descendant_groups[0].path == subgroup_descgroup_content[0]["path"] @pytest.mark.skip("GitLab API endpoint not implemented") def test_refresh_group_export_status(group, resp_export): export = group.exports.create() export.refresh() assert export.export_status == "finished" def test_download_group_export(group, resp_export, binary_content): export = group.exports.create() download = export.download() assert isinstance(download, bytes) assert download == binary_content def test_import_group(gl, resp_create_import): group_import = gl.groups.import_group("file", "api-group", "API Group") assert group_import["message"] == "202 Accepted" @pytest.mark.skip("GitLab API endpoint not implemented") def test_refresh_group_import_status(group, resp_groups): group_import = group.imports.get() group_import.refresh() assert group_import.import_status == "finished" python-gitlab-2.10.1/tests/unit/objects/test_hooks.py000066400000000000000000000122251416141341200226610ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ce/api/system_hooks.html GitLab API: https://docs.gitlab.com/ce/api/groups.html#hooks GitLab API: https://docs.gitlab.com/ee/api/projects.html#hooks """ import re import pytest import responses from gitlab.v4.objects import GroupHook, Hook, ProjectHook hooks_content = [ { "id": 1, "url": "testurl", "push_events": True, "tag_push_events": True, }, { "id": 2, "url": "testurl_second", "push_events": False, "tag_push_events": False, }, ] hook_content = hooks_content[0] @pytest.fixture def resp_hooks_list(): with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url=re.compile(r"http://localhost/api/v4/((groups|projects)/1/|)hooks"), json=hooks_content, content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_hook_get(): with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url=re.compile(r"http://localhost/api/v4/((groups|projects)/1/|)hooks/1"), json=hook_content, content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_hook_create(): with responses.RequestsMock() as rsps: rsps.add( method=responses.POST, url=re.compile(r"http://localhost/api/v4/((groups|projects)/1/|)hooks"), json=hook_content, content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_hook_update(): with responses.RequestsMock() as rsps: pattern = re.compile(r"http://localhost/api/v4/((groups|projects)/1/|)hooks/1") rsps.add( method=responses.GET, url=pattern, json=hook_content, content_type="application/json", status=200, ) rsps.add( method=responses.PUT, url=pattern, json=hook_content, content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_hook_delete(): with responses.RequestsMock() as rsps: pattern = re.compile(r"http://localhost/api/v4/((groups|projects)/1/|)hooks/1") rsps.add( method=responses.GET, url=pattern, json=hook_content, content_type="application/json", status=200, ) rsps.add( method=responses.DELETE, url=pattern, status=204, ) yield rsps def test_list_system_hooks(gl, resp_hooks_list): hooks = gl.hooks.list() assert hooks[0].id == 1 assert hooks[0].url == "testurl" assert hooks[1].id == 2 assert hooks[1].url == "testurl_second" def test_get_system_hook(gl, resp_hook_get): data = gl.hooks.get(1) assert isinstance(data, Hook) assert data.url == "testurl" assert data.id == 1 def test_create_system_hook(gl, resp_hook_create): hook = gl.hooks.create(hook_content) assert hook.url == "testurl" assert hook.push_events is True assert hook.tag_push_events is True # there is no update method for system hooks def test_delete_system_hook(gl, resp_hook_delete): hook = gl.hooks.get(1) hook.delete() gl.hooks.delete(1) def test_list_group_hooks(group, resp_hooks_list): hooks = group.hooks.list() assert hooks[0].id == 1 assert hooks[0].url == "testurl" assert hooks[1].id == 2 assert hooks[1].url == "testurl_second" def test_get_group_hook(group, resp_hook_get): data = group.hooks.get(1) assert isinstance(data, GroupHook) assert data.url == "testurl" assert data.id == 1 def test_create_group_hook(group, resp_hook_create): hook = group.hooks.create(hook_content) assert hook.url == "testurl" assert hook.push_events is True assert hook.tag_push_events is True def test_update_group_hook(group, resp_hook_update): hook = group.hooks.get(1) assert hook.id == 1 hook.url = "testurl_more" hook.save() def test_delete_group_hook(group, resp_hook_delete): hook = group.hooks.get(1) hook.delete() group.hooks.delete(1) def test_list_project_hooks(project, resp_hooks_list): hooks = project.hooks.list() assert hooks[0].id == 1 assert hooks[0].url == "testurl" assert hooks[1].id == 2 assert hooks[1].url == "testurl_second" def test_get_project_hook(project, resp_hook_get): data = project.hooks.get(1) assert isinstance(data, ProjectHook) assert data.url == "testurl" assert data.id == 1 def test_create_project_hook(project, resp_hook_create): hook = project.hooks.create(hook_content) assert hook.url == "testurl" assert hook.push_events is True assert hook.tag_push_events is True def test_update_project_hook(project, resp_hook_update): hook = project.hooks.get(1) assert hook.id == 1 hook.url = "testurl_more" hook.save() def test_delete_project_hook(project, resp_hook_delete): hook = project.hooks.get(1) hook.delete() project.hooks.delete(1) python-gitlab-2.10.1/tests/unit/objects/test_issues.py000066400000000000000000000046751416141341200230630ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ce/api/issues.html """ import re import pytest import responses from gitlab.v4.objects import ( GroupIssuesStatistics, IssuesStatistics, ProjectIssuesStatistics, ) @pytest.fixture def resp_list_issues(): content = [{"name": "name", "id": 1}, {"name": "other_name", "id": 2}] with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/issues", json=content, content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_get_issue(): with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/issues/1", json={"name": "name", "id": 1}, content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_issue_statistics(): content = {"statistics": {"counts": {"all": 20, "closed": 5, "opened": 15}}} with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url=re.compile( r"http://localhost/api/v4/((groups|projects)/1/)?issues_statistics" ), json=content, content_type="application/json", status=200, ) yield rsps def test_list_issues(gl, resp_list_issues): data = gl.issues.list() assert data[1].id == 2 assert data[1].name == "other_name" def test_get_issue(gl, resp_get_issue): issue = gl.issues.get(1) assert issue.id == 1 assert issue.name == "name" def test_get_issues_statistics(gl, resp_issue_statistics): statistics = gl.issues_statistics.get() assert isinstance(statistics, IssuesStatistics) assert statistics.statistics["counts"]["all"] == 20 def test_get_group_issues_statistics(group, resp_issue_statistics): statistics = group.issues_statistics.get() assert isinstance(statistics, GroupIssuesStatistics) assert statistics.statistics["counts"]["all"] == 20 def test_get_project_issues_statistics(project, resp_issue_statistics): statistics = project.issues_statistics.get() assert isinstance(statistics, ProjectIssuesStatistics) assert statistics.statistics["counts"]["all"] == 20 # Deprecated attribute deprecated = project.issuesstatistics.get() assert deprecated.statistics == statistics.statistics python-gitlab-2.10.1/tests/unit/objects/test_job_artifacts.py000066400000000000000000000014241416141341200243470ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ee/api/job_artifacts.html """ import pytest import responses ref_name = "master" job = "build" @pytest.fixture def resp_artifacts_by_ref_name(binary_content): url = f"http://localhost/api/v4/projects/1/jobs/artifacts/{ref_name}/download?job={job}" with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url=url, body=binary_content, content_type="application/octet-stream", status=200, ) yield rsps def test_download_artifacts_by_ref_name(gl, binary_content, resp_artifacts_by_ref_name): project = gl.projects.get(1, lazy=True) artifacts = project.artifacts(ref_name=ref_name, job=job) assert artifacts == binary_content python-gitlab-2.10.1/tests/unit/objects/test_jobs.py000066400000000000000000000045041416141341200224740ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ee/api/jobs.html """ import pytest import responses from gitlab.v4.objects import ProjectJob job_content = { "commit": { "author_email": "admin@example.com", "author_name": "Administrator", }, "coverage": None, "allow_failure": False, "created_at": "2015-12-24T15:51:21.880Z", "started_at": "2015-12-24T17:54:30.733Z", "finished_at": "2015-12-24T17:54:31.198Z", "duration": 0.465, "queued_duration": 0.010, "artifacts_expire_at": "2016-01-23T17:54:31.198Z", "tag_list": ["docker runner", "macos-10.15"], "id": 1, "name": "rubocop", "pipeline": { "id": 1, "project_id": 1, }, "ref": "master", "artifacts": [], "runner": None, "stage": "test", "status": "failed", "tag": False, "web_url": "https://example.com/foo/bar/-/jobs/1", "user": {"id": 1}, } @pytest.fixture def resp_get_job(): with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/jobs/1", json=job_content, content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_cancel_job(): with responses.RequestsMock() as rsps: rsps.add( method=responses.POST, url="http://localhost/api/v4/projects/1/jobs/1/cancel", json=job_content, content_type="application/json", status=201, ) yield rsps @pytest.fixture def resp_retry_job(): with responses.RequestsMock() as rsps: rsps.add( method=responses.POST, url="http://localhost/api/v4/projects/1/jobs/1/retry", json=job_content, content_type="application/json", status=201, ) yield rsps def test_get_project_job(project, resp_get_job): job = project.jobs.get(1) assert isinstance(job, ProjectJob) assert job.ref == "master" def test_cancel_project_job(project, resp_cancel_job): job = project.jobs.get(1, lazy=True) output = job.cancel() assert output["ref"] == "master" def test_retry_project_job(project, resp_retry_job): job = project.jobs.get(1, lazy=True) output = job.retry() assert output["ref"] == "master" python-gitlab-2.10.1/tests/unit/objects/test_keys.py000066400000000000000000000024611416141341200225120ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ce/api/keys.html """ import pytest import responses from gitlab.v4.objects import Key key_content = {"id": 1, "title": "title", "key": "ssh-keytype AAAAC3Nza/key comment"} @pytest.fixture def resp_get_key_by_id(): with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/keys/1", json=key_content, content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_get_key_by_fingerprint(): with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/keys?fingerprint=foo", json=key_content, content_type="application/json", status=200, ) yield rsps def test_get_key_by_id(gl, resp_get_key_by_id): key = gl.keys.get(1) assert isinstance(key, Key) assert key.id == 1 assert key.title == "title" def test_get_key_by_fingerprint(gl, resp_get_key_by_fingerprint): key = gl.keys.get(fingerprint="foo") assert isinstance(key, Key) assert key.id == 1 assert key.title == "title" def test_get_key_missing_attrs(gl): with pytest.raises(AttributeError): gl.keys.get() python-gitlab-2.10.1/tests/unit/objects/test_members.py000066400000000000000000000032631416141341200231720ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ee/api/members.html """ import pytest import responses from gitlab.v4.objects import GroupBillableMember billable_members_content = [ { "id": 1, "username": "raymond_smith", "name": "Raymond Smith", "state": "active", "avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon", "web_url": "http://192.168.1.8:3000/root", "last_activity_on": "2021-01-27", "membership_type": "group_member", "removable": True, } ] @pytest.fixture def resp_list_billable_group_members(): with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/groups/1/billable_members", json=billable_members_content, content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_delete_billable_group_member(no_content): with responses.RequestsMock() as rsps: rsps.add( method=responses.DELETE, url="http://localhost/api/v4/groups/1/billable_members/1", json=no_content, content_type="application/json", status=204, ) yield rsps def test_list_group_billable_members(group, resp_list_billable_group_members): billable_members = group.billable_members.list() assert isinstance(billable_members, list) assert isinstance(billable_members[0], GroupBillableMember) assert billable_members[0].removable is True def test_delete_group_billable_member(group, resp_delete_billable_group_member): group.billable_members.delete(1) python-gitlab-2.10.1/tests/unit/objects/test_merge_request_pipelines.py000066400000000000000000000037421416141341200264610ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ee/api/merge_requests.html#list-mr-pipelines """ import pytest import responses from gitlab.v4.objects import ProjectMergeRequestPipeline pipeline_content = { "id": 1, "sha": "959e04d7c7a30600c894bd3c0cd0e1ce7f42c11d", "ref": "master", "status": "success", } @pytest.fixture() def resp_list_merge_request_pipelines(): with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/merge_requests/1/pipelines", json=[pipeline_content], content_type="application/json", status=200, ) yield rsps @pytest.fixture() def resp_create_merge_request_pipeline(): with responses.RequestsMock() as rsps: rsps.add( method=responses.POST, url="http://localhost/api/v4/projects/1/merge_requests/1/pipelines", json=pipeline_content, content_type="application/json", status=201, ) yield rsps def test_merge_requests_pipelines_deprecated_raises_warning( project, resp_list_merge_request_pipelines ): with pytest.deprecated_call(): pipelines = project.mergerequests.get(1, lazy=True).pipelines() assert len(pipelines) == 1 assert isinstance(pipelines[0], ProjectMergeRequestPipeline) assert pipelines[0].sha == pipeline_content["sha"] def test_list_merge_requests_pipelines(project, resp_list_merge_request_pipelines): pipelines = project.mergerequests.get(1, lazy=True).pipelines.list() assert len(pipelines) == 1 assert isinstance(pipelines[0], ProjectMergeRequestPipeline) assert pipelines[0].sha == pipeline_content["sha"] def test_create_merge_requests_pipelines(project, resp_create_merge_request_pipeline): pipeline = project.mergerequests.get(1, lazy=True).pipelines.create() assert isinstance(pipeline, ProjectMergeRequestPipeline) assert pipeline.sha == pipeline_content["sha"] python-gitlab-2.10.1/tests/unit/objects/test_merge_requests.py000066400000000000000000000031341416141341200245670ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ce/api/merge_requests.html https://docs.gitlab.com/ee/api/deployments.html#list-of-merge-requests-associated-with-a-deployment """ import re import pytest import responses from gitlab.v4.objects import ProjectDeploymentMergeRequest, ProjectMergeRequest mr_content = { "id": 1, "iid": 1, "project_id": 3, "title": "test1", "description": "fixed login page css paddings", "state": "merged", "merged_by": { "id": 87854, "name": "Douwe Maan", "username": "DouweM", "state": "active", "avatar_url": "https://gitlab.example.com/uploads/-/system/user/avatar/87854/avatar.png", "web_url": "https://gitlab.com/DouweM", }, } @pytest.fixture def resp_list_merge_requests(): with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url=re.compile( r"http://localhost/api/v4/projects/1/(deployments/1/)?merge_requests" ), json=[mr_content], content_type="application/json", status=200, ) yield rsps def test_list_project_merge_requests(project, resp_list_merge_requests): mrs = project.mergerequests.list() assert isinstance(mrs[0], ProjectMergeRequest) assert mrs[0].iid == mr_content["iid"] def test_list_deployment_merge_requests(project, resp_list_merge_requests): deployment = project.deployments.get(1, lazy=True) mrs = deployment.mergerequests.list() assert isinstance(mrs[0], ProjectDeploymentMergeRequest) assert mrs[0].iid == mr_content["iid"] python-gitlab-2.10.1/tests/unit/objects/test_mro.py000066400000000000000000000076471416141341200223470ustar00rootroot00000000000000""" Ensure objects defined in gitlab.v4.objects have REST* as last item in class definition Original notes by John L. Villalovos An example of an incorrect definition: class ProjectPipeline(RESTObject, RefreshMixin, ObjectDeleteMixin): ^^^^^^^^^^ This should be at the end. Correct way would be: class ProjectPipeline(RefreshMixin, ObjectDeleteMixin, RESTObject): Correctly at the end ^^^^^^^^^^ Why this is an issue: When we do type-checking for gitlab/mixins.py we make RESTObject or RESTManager the base class for the mixins Here is how our classes look when type-checking: class RESTObject(object): def __init__(self, manager: "RESTManager", attrs: Dict[str, Any]) -> None: ... class Mixin(RESTObject): ... # Wrong ordering here class Wrongv4Object(RESTObject, RefreshMixin): ... If we actually ran this in Python we would get the following error: class Wrongv4Object(RESTObject, Mixin): TypeError: Cannot create a consistent method resolution order (MRO) for bases RESTObject, Mixin When we are type-checking it fails to understand the class Wrongv4Object and thus we can't type check it correctly. Almost all classes in gitlab/v4/objects/*py were already correct before this check was added. """ import inspect import pytest import gitlab.v4.objects def test_show_issue(): """Test case to demonstrate the TypeError that occurs""" class RESTObject(object): def __init__(self, manager: str, attrs: int) -> None: ... class Mixin(RESTObject): ... with pytest.raises(TypeError) as exc_info: # Wrong ordering here class Wrongv4Object(RESTObject, Mixin): ... # The error message in the exception should be: # TypeError: Cannot create a consistent method resolution # order (MRO) for bases RESTObject, Mixin # Make sure the exception string contains "MRO" assert "MRO" in exc_info.exconly() # Correctly ordered class, no exception class Correctv4Object(Mixin, RESTObject): ... def test_mros(): """Ensure objects defined in gitlab.v4.objects have REST* as last item in class definition. We do this as we need to ensure the MRO (Method Resolution Order) is correct. """ failed_messages = [] for module_name, module_value in inspect.getmembers(gitlab.v4.objects): if not inspect.ismodule(module_value): # We only care about the modules continue # Iterate through all the classes in our module for class_name, class_value in inspect.getmembers(module_value): if not inspect.isclass(class_value): continue # Ignore imported classes from gitlab.base if class_value.__module__ == "gitlab.base": continue mro = class_value.mro() # We only check classes which have a 'gitlab.base' class in their MRO has_base = False for count, obj in enumerate(mro, start=1): if obj.__module__ == "gitlab.base": has_base = True base_classname = obj.__name__ if has_base: filename = inspect.getfile(class_value) # NOTE(jlvillal): The very last item 'mro[-1]' is always going # to be 'object'. That is why we are checking 'mro[-2]'. if mro[-2].__module__ != "gitlab.base": failed_messages.append( ( f"class definition for {class_name!r} in file {filename!r} " f"must have {base_classname!r} as the last class in the " f"class definition" ) ) failed_msg = "\n".join(failed_messages) assert not failed_messages, failed_msg python-gitlab-2.10.1/tests/unit/objects/test_packages.py000066400000000000000000000170101416141341200233110ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ce/api/packages.html """ import re from urllib.parse import quote_plus import pytest import responses from gitlab.v4.objects import ( GenericPackage, GroupPackage, ProjectPackage, ProjectPackageFile, ) package_content = { "id": 1, "name": "com/mycompany/my-app", "version": "1.0-SNAPSHOT", "package_type": "maven", "_links": { "web_path": "/namespace1/project1/-/packages/1", "delete_api_path": "/namespace1/project1/-/packages/1", }, "created_at": "2019-11-27T03:37:38.711Z", "pipeline": { "id": 123, "status": "pending", "ref": "new-pipeline", "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a", "web_url": "https://example.com/foo/bar/pipelines/47", "created_at": "2016-08-11T11:28:34.085Z", "updated_at": "2016-08-11T11:32:35.169Z", "user": { "name": "Administrator", "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", }, }, "versions": [ { "id": 2, "version": "2.0-SNAPSHOT", "created_at": "2020-04-28T04:42:11.573Z", "pipeline": { "id": 234, "status": "pending", "ref": "new-pipeline", "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a", "web_url": "https://example.com/foo/bar/pipelines/58", "created_at": "2016-08-11T11:28:34.085Z", "updated_at": "2016-08-11T11:32:35.169Z", "user": { "name": "Administrator", "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", }, }, } ], } package_file_content = [ { "id": 25, "package_id": 1, "created_at": "2018-11-07T15:25:52.199Z", "file_name": "my-app-1.5-20181107.152550-1.jar", "size": 2421, "file_md5": "58e6a45a629910c6ff99145a688971ac", "file_sha1": "ebd193463d3915d7e22219f52740056dfd26cbfe", "pipelines": [ { "id": 123, "status": "pending", "ref": "new-pipeline", "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a", "web_url": "https://example.com/foo/bar/pipelines/47", "created_at": "2016-08-11T11:28:34.085Z", "updated_at": "2016-08-11T11:32:35.169Z", "user": { "name": "Administrator", "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", }, } ], }, { "id": 26, "package_id": 1, "created_at": "2018-11-07T15:25:56.776Z", "file_name": "my-app-1.5-20181107.152550-1.pom", "size": 1122, "file_md5": "d90f11d851e17c5513586b4a7e98f1b2", "file_sha1": "9608d068fe88aff85781811a42f32d97feb440b5", }, { "id": 27, "package_id": 1, "created_at": "2018-11-07T15:26:00.556Z", "file_name": "maven-metadata.xml", "size": 767, "file_md5": "6dfd0cce1203145a927fef5e3a1c650c", "file_sha1": "d25932de56052d320a8ac156f745ece73f6a8cd2", }, ] package_name = "hello-world" package_version = "v1.0.0" file_name = "hello.tar.gz" file_content = "package content" package_url = "http://localhost/api/v4/projects/1/packages/generic/{}/{}/{}".format( # https://datatracker.ietf.org/doc/html/rfc3986.html#section-2.3 :( quote_plus(package_name).replace(".", "%2E"), quote_plus(package_version).replace(".", "%2E"), quote_plus(file_name).replace(".", "%2E"), ) @pytest.fixture def resp_list_packages(): with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url=re.compile(r"http://localhost/api/v4/(groups|projects)/1/packages"), json=[package_content], content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_get_package(): with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/packages/1", json=package_content, content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_delete_package(no_content): with responses.RequestsMock() as rsps: rsps.add( method=responses.DELETE, url="http://localhost/api/v4/projects/1/packages/1", json=no_content, content_type="application/json", status=204, ) yield rsps @pytest.fixture def resp_list_package_files(): with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url=re.compile( r"http://localhost/api/v4/projects/1/packages/1/package_files" ), json=package_file_content, content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_upload_generic_package(created_content): with responses.RequestsMock() as rsps: rsps.add( method=responses.PUT, url=package_url, json=created_content, content_type="application/json", status=201, ) yield rsps @pytest.fixture def resp_download_generic_package(created_content): with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url=package_url, body=file_content, content_type="application/octet-stream", status=200, ) yield rsps def test_list_project_packages(project, resp_list_packages): packages = project.packages.list() assert isinstance(packages, list) assert isinstance(packages[0], ProjectPackage) assert packages[0].version == "1.0-SNAPSHOT" def test_list_group_packages(group, resp_list_packages): packages = group.packages.list() assert isinstance(packages, list) assert isinstance(packages[0], GroupPackage) assert packages[0].version == "1.0-SNAPSHOT" def test_get_project_package(project, resp_get_package): package = project.packages.get(1) assert isinstance(package, ProjectPackage) assert package.version == "1.0-SNAPSHOT" def test_delete_project_package(project, resp_delete_package): package = project.packages.get(1, lazy=True) package.delete() def test_list_project_package_files(project, resp_list_package_files): package = project.packages.get(1, lazy=True) package_files = package.package_files.list() assert isinstance(package_files, list) assert isinstance(package_files[0], ProjectPackageFile) assert package_files[0].id == 25 def test_upload_generic_package(tmp_path, project, resp_upload_generic_package): path = tmp_path / file_name path.write_text(file_content) package = project.generic_packages.upload( package_name=package_name, package_version=package_version, file_name=file_name, path=path, ) assert isinstance(package, GenericPackage) def test_download_generic_package(project, resp_download_generic_package): package = project.generic_packages.download( package_name=package_name, package_version=package_version, file_name=file_name, ) assert isinstance(package, bytes) python-gitlab-2.10.1/tests/unit/objects/test_personal_access_tokens.py000066400000000000000000000024441416141341200262670ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ee/api/personal_access_tokens.html """ import pytest import responses @pytest.fixture def resp_list_personal_access_token(): content = [ { "id": 4, "name": "Test Token", "revoked": False, "created_at": "2020-07-23T14:31:47.729Z", "scopes": ["api"], "active": True, "user_id": 24, "expires_at": None, } ] with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/personal_access_tokens", json=content, content_type="application/json", status=200, ) yield rsps def test_list_personal_access_tokens(gl, resp_list_personal_access_token): access_tokens = gl.personal_access_tokens.list() assert len(access_tokens) == 1 assert access_tokens[0].revoked is False assert access_tokens[0].name == "Test Token" def test_list_personal_access_tokens_filter(gl, resp_list_personal_access_token): access_tokens = gl.personal_access_tokens.list(user_id=24) assert len(access_tokens) == 1 assert access_tokens[0].revoked is False assert access_tokens[0].user_id == 24 python-gitlab-2.10.1/tests/unit/objects/test_pipeline_schedules.py000066400000000000000000000036251416141341200254060ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ce/api/pipeline_schedules.html """ import pytest import responses @pytest.fixture def resp_project_pipeline_schedule(created_content): content = { "id": 14, "description": "Build packages", "ref": "master", "cron": "0 1 * * 5", "cron_timezone": "UTC", "next_run_at": "2017-05-26T01:00:00.000Z", "active": True, "created_at": "2017-05-19T13:43:08.169Z", "updated_at": "2017-05-19T13:43:08.169Z", "last_pipeline": None, "owner": { "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", }, } with responses.RequestsMock() as rsps: rsps.add( method=responses.POST, url="http://localhost/api/v4/projects/1/pipeline_schedules", json=content, content_type="application/json", status=200, ) rsps.add( method=responses.POST, url="http://localhost/api/v4/projects/1/pipeline_schedules/14/play", json=created_content, content_type="application/json", status=201, ) yield rsps def test_project_pipeline_schedule_play(project, resp_project_pipeline_schedule): description = "Build packages" cronline = "0 1 * * 5" sched = project.pipelineschedules.create( {"ref": "master", "description": description, "cron": cronline} ) assert sched is not None assert description == sched.description assert cronline == sched.cron play_result = sched.play() assert play_result is not None assert "message" in play_result assert play_result["message"] == "201 Created" python-gitlab-2.10.1/tests/unit/objects/test_pipelines.py000066400000000000000000000100661416141341200235270ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ee/api/pipelines.html """ import pytest import responses from gitlab.v4.objects import ProjectPipeline, ProjectPipelineTestReport pipeline_content = { "id": 46, "project_id": 1, "status": "pending", "ref": "master", "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a", "before_sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a", "tag": False, "yaml_errors": None, "user": { "name": "Administrator", "username": "root", "id": 1, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "web_url": "http://localhost:3000/root", }, "created_at": "2016-08-11T11:28:34.085Z", "updated_at": "2016-08-11T11:32:35.169Z", "started_at": None, "finished_at": "2016-08-11T11:32:35.145Z", "committed_at": None, "duration": None, "queued_duration": 0.010, "coverage": None, "web_url": "https://example.com/foo/bar/pipelines/46", } test_report_content = { "total_time": 5, "total_count": 1, "success_count": 1, "failed_count": 0, "skipped_count": 0, "error_count": 0, "test_suites": [ { "name": "Secure", "total_time": 5, "total_count": 1, "success_count": 1, "failed_count": 0, "skipped_count": 0, "error_count": 0, "test_cases": [ { "status": "success", "name": "Security Reports can create an auto-remediation MR", "classname": "vulnerability_management_spec", "execution_time": 5, "system_output": None, "stack_trace": None, } ], } ], } @pytest.fixture def resp_get_pipeline(): with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/pipelines/1", json=pipeline_content, content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_cancel_pipeline(): with responses.RequestsMock() as rsps: rsps.add( method=responses.POST, url="http://localhost/api/v4/projects/1/pipelines/1/cancel", json=pipeline_content, content_type="application/json", status=201, ) yield rsps @pytest.fixture def resp_retry_pipeline(): with responses.RequestsMock() as rsps: rsps.add( method=responses.POST, url="http://localhost/api/v4/projects/1/pipelines/1/retry", json=pipeline_content, content_type="application/json", status=201, ) yield rsps @pytest.fixture def resp_get_pipeline_test_report(): with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/pipelines/1/test_report", json=test_report_content, content_type="application/json", status=200, ) yield rsps def test_get_project_pipeline(project, resp_get_pipeline): pipeline = project.pipelines.get(1) assert isinstance(pipeline, ProjectPipeline) assert pipeline.ref == "master" def test_cancel_project_pipeline(project, resp_cancel_pipeline): pipeline = project.pipelines.get(1, lazy=True) output = pipeline.cancel() assert output["ref"] == "master" def test_retry_project_pipeline(project, resp_retry_pipeline): pipeline = project.pipelines.get(1, lazy=True) output = pipeline.retry() assert output["ref"] == "master" def test_get_project_pipeline_test_report(project, resp_get_pipeline_test_report): pipeline = project.pipelines.get(1, lazy=True) test_report = pipeline.test_report.get() assert isinstance(test_report, ProjectPipelineTestReport) assert test_report.total_time == 5 assert test_report.test_suites[0]["name"] == "Secure" python-gitlab-2.10.1/tests/unit/objects/test_project_access_tokens.py000066400000000000000000000062261416141341200261140ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ee/api/resource_access_tokens.html """ import pytest import responses @pytest.fixture def resp_list_project_access_token(): content = [ { "user_id": 141, "scopes": ["api"], "name": "token", "expires_at": "2021-01-31", "id": 42, "active": True, "created_at": "2021-01-20T22:11:48.151Z", "revoked": False, } ] with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/access_tokens", json=content, content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_create_project_access_token(): content = { "user_id": 141, "scopes": ["api"], "name": "token", "expires_at": "2021-01-31", "id": 42, "active": True, "created_at": "2021-01-20T22:11:48.151Z", "revoked": False, } with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: rsps.add( method=responses.POST, url="http://localhost/api/v4/projects/1/access_tokens", json=content, content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_revoke_project_access_token(): content = [ { "user_id": 141, "scopes": ["api"], "name": "token", "expires_at": "2021-01-31", "id": 42, "active": True, "created_at": "2021-01-20T22:11:48.151Z", "revoked": False, } ] with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: rsps.add( method=responses.DELETE, url="http://localhost/api/v4/projects/1/access_tokens/42", json=content, content_type="application/json", status=204, ) rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/access_tokens", json=content, content_type="application/json", status=200, ) yield rsps def test_list_project_access_tokens(gl, resp_list_project_access_token): access_tokens = gl.projects.get(1, lazy=True).access_tokens.list() assert len(access_tokens) == 1 assert access_tokens[0].revoked is False assert access_tokens[0].name == "token" def test_create_project_access_token(gl, resp_create_project_access_token): access_tokens = gl.projects.get(1, lazy=True).access_tokens.create( {"name": "test", "scopes": ["api"]} ) assert access_tokens.revoked is False assert access_tokens.user_id == 141 assert access_tokens.expires_at == "2021-01-31" def test_revoke_project_access_token( gl, resp_list_project_access_token, resp_revoke_project_access_token ): gl.projects.get(1, lazy=True).access_tokens.delete(42) access_token = gl.projects.get(1, lazy=True).access_tokens.list()[0] access_token.delete() python-gitlab-2.10.1/tests/unit/objects/test_project_import_export.py000066400000000000000000000061731416141341200262040ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ce/api/project_import_export.html """ import pytest import responses @pytest.fixture def resp_import_project(): content = { "id": 1, "description": None, "name": "api-project", "name_with_namespace": "Administrator / api-project", "path": "api-project", "path_with_namespace": "root/api-project", "created_at": "2018-02-13T09:05:58.023Z", "import_status": "scheduled", } with responses.RequestsMock() as rsps: rsps.add( method=responses.POST, url="http://localhost/api/v4/projects/import", json=content, content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_import_status(): content = { "id": 1, "description": "Itaque perspiciatis minima aspernatur corporis consequatur.", "name": "Gitlab Test", "name_with_namespace": "Gitlab Org / Gitlab Test", "path": "gitlab-test", "path_with_namespace": "gitlab-org/gitlab-test", "created_at": "2017-08-29T04:36:44.383Z", "import_status": "finished", } with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/import", json=content, content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_import_github(): content = { "id": 27, "name": "my-repo", "full_path": "/root/my-repo", "full_name": "Administrator / my-repo", } with responses.RequestsMock() as rsps: rsps.add( method=responses.POST, url="http://localhost/api/v4/import/github", json=content, content_type="application/json", status=200, ) yield rsps def test_import_project(gl, resp_import_project): project_import = gl.projects.import_project("file", "api-project") assert project_import["import_status"] == "scheduled" def test_refresh_project_import_status(project, resp_import_status): project_import = project.imports.get() project_import.refresh() assert project_import.import_status == "finished" def test_import_github(gl, resp_import_github): base_path = "/root" name = "my-repo" ret = gl.projects.import_github("githubkey", 1234, base_path, name) assert isinstance(ret, dict) assert ret["name"] == name assert ret["full_path"] == "/".join((base_path, name)) assert ret["full_name"].endswith(name) def test_create_project_export(project, resp_export): export = project.exports.create() assert export.message == "202 Accepted" def test_refresh_project_export_status(project, resp_export): export = project.exports.create() export.refresh() assert export.export_status == "finished" def test_download_project_export(project, resp_export, binary_content): export = project.exports.create() download = export.download() assert isinstance(download, bytes) assert download == binary_content python-gitlab-2.10.1/tests/unit/objects/test_project_merge_request_approvals.py000066400000000000000000000271331416141341200302260ustar00rootroot00000000000000""" Gitlab API: https://docs.gitlab.com/ee/api/merge_request_approvals.html """ import copy import pytest import responses import gitlab approval_rule_id = 1 approval_rule_name = "security" approvals_required = 3 user_ids = [5, 50] group_ids = [5] new_approval_rule_name = "new approval rule" new_approval_rule_user_ids = user_ids new_approval_rule_approvals_required = 2 updated_approval_rule_user_ids = [5] updated_approval_rule_approvals_required = 1 @pytest.fixture def resp_snippet(): merge_request_content = [ { "id": 1, "iid": 1, "project_id": 1, "title": "test1", "description": "fixed login page css paddings", "state": "merged", "merged_by": { "id": 87854, "name": "Douwe Maan", "username": "DouweM", "state": "active", "avatar_url": "https://gitlab.example.com/uploads/-/system/user/avatar/87854/avatar.png", "web_url": "https://gitlab.com/DouweM", }, "merged_at": "2018-09-07T11:16:17.520Z", "closed_by": None, "closed_at": None, "created_at": "2017-04-29T08:46:00Z", "updated_at": "2017-04-29T08:46:00Z", "target_branch": "master", "source_branch": "test1", "upvotes": 0, "downvotes": 0, "author": { "id": 1, "name": "Administrator", "username": "admin", "state": "active", "avatar_url": None, "web_url": "https://gitlab.example.com/admin", }, "assignee": { "id": 1, "name": "Administrator", "username": "admin", "state": "active", "avatar_url": None, "web_url": "https://gitlab.example.com/admin", }, "assignees": [ { "name": "Miss Monserrate Beier", "username": "axel.block", "id": 12, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon", "web_url": "https://gitlab.example.com/axel.block", } ], "source_project_id": 2, "target_project_id": 3, "labels": ["Community contribution", "Manage"], "work_in_progress": None, "milestone": { "id": 5, "iid": 1, "project_id": 3, "title": "v2.0", "description": "Assumenda aut placeat expedita exercitationem labore sunt enim earum.", "state": "closed", "created_at": "2015-02-02T19:49:26.013Z", "updated_at": "2015-02-02T19:49:26.013Z", "due_date": "2018-09-22", "start_date": "2018-08-08", "web_url": "https://gitlab.example.com/my-group/my-project/milestones/1", }, "merge_when_pipeline_succeeds": None, "merge_status": "can_be_merged", "sha": "8888888888888888888888888888888888888888", "merge_commit_sha": None, "squash_commit_sha": None, "user_notes_count": 1, "discussion_locked": None, "should_remove_source_branch": True, "force_remove_source_branch": False, "allow_collaboration": False, "allow_maintainer_to_push": False, "web_url": "http://gitlab.example.com/my-group/my-project/merge_requests/1", "references": { "short": "!1", "relative": "my-group/my-project!1", "full": "my-group/my-project!1", }, "time_stats": { "time_estimate": 0, "total_time_spent": 0, "human_time_estimate": None, "human_total_time_spent": None, }, "squash": False, "task_completion_status": {"count": 0, "completed_count": 0}, } ] mr_ars_content = [ { "id": approval_rule_id, "name": approval_rule_name, "rule_type": "regular", "eligible_approvers": [ { "id": user_ids[0], "name": "John Doe", "username": "jdoe", "state": "active", "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", "web_url": "http://localhost/jdoe", }, { "id": user_ids[1], "name": "Group Member 1", "username": "group_member_1", "state": "active", "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", "web_url": "http://localhost/group_member_1", }, ], "approvals_required": approvals_required, "source_rule": None, "users": [ { "id": 5, "name": "John Doe", "username": "jdoe", "state": "active", "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", "web_url": "http://localhost/jdoe", } ], "groups": [ { "id": 5, "name": "group1", "path": "group1", "description": "", "visibility": "public", "lfs_enabled": False, "avatar_url": None, "web_url": "http://localhost/groups/group1", "request_access_enabled": False, "full_name": "group1", "full_path": "group1", "parent_id": None, "ldap_cn": None, "ldap_access": None, } ], "contains_hidden_groups": False, "overridden": False, } ] with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/merge_requests", json=merge_request_content, content_type="application/json", status=200, ) rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/merge_requests/1", json=merge_request_content[0], content_type="application/json", status=200, ) rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/merge_requests/1/approval_rules", json=mr_ars_content, content_type="application/json", status=200, ) new_mr_ars_content = dict(mr_ars_content[0]) new_mr_ars_content["name"] = new_approval_rule_name new_mr_ars_content["approvals_required"] = new_approval_rule_approvals_required rsps.add( method=responses.POST, url="http://localhost/api/v4/projects/1/merge_requests/1/approval_rules", json=new_mr_ars_content, content_type="application/json", status=200, ) updated_mr_ars_content = copy.deepcopy(mr_ars_content[0]) updated_mr_ars_content["eligible_approvers"] = [ mr_ars_content[0]["eligible_approvers"][0] ] updated_mr_ars_content[ "approvals_required" ] = updated_approval_rule_approvals_required rsps.add( method=responses.PUT, url="http://localhost/api/v4/projects/1/merge_requests/1/approval_rules/1", json=updated_mr_ars_content, content_type="application/json", status=200, ) yield rsps def test_project_approval_manager_update_uses_post(project, resp_snippet): """Ensure the gitlab.v4.objects.merge_request_approvals.ProjectApprovalManager object has _update_uses_post set to True""" approvals = project.approvals assert isinstance( approvals, gitlab.v4.objects.merge_request_approvals.ProjectApprovalManager ) assert approvals._update_uses_post is True def test_list_merge_request_approval_rules(project, resp_snippet): approval_rules = project.mergerequests.get(1).approval_rules.list() assert len(approval_rules) == 1 assert approval_rules[0].name == approval_rule_name assert approval_rules[0].id == approval_rule_id def test_update_merge_request_approvals_set_approvers(project, resp_snippet): approvals = project.mergerequests.get(1).approvals assert isinstance( approvals, gitlab.v4.objects.merge_request_approvals.ProjectMergeRequestApprovalManager, ) assert approvals._update_uses_post is True response = approvals.set_approvers( updated_approval_rule_approvals_required, approver_ids=updated_approval_rule_user_ids, approver_group_ids=group_ids, approval_rule_name=approval_rule_name, ) assert response.approvals_required == updated_approval_rule_approvals_required assert len(response.eligible_approvers) == len(updated_approval_rule_user_ids) assert response.eligible_approvers[0]["id"] == updated_approval_rule_user_ids[0] assert response.name == approval_rule_name def test_create_merge_request_approvals_set_approvers(project, resp_snippet): approvals = project.mergerequests.get(1).approvals assert isinstance( approvals, gitlab.v4.objects.merge_request_approvals.ProjectMergeRequestApprovalManager, ) assert approvals._update_uses_post is True response = approvals.set_approvers( new_approval_rule_approvals_required, approver_ids=new_approval_rule_user_ids, approver_group_ids=group_ids, approval_rule_name=new_approval_rule_name, ) assert response.approvals_required == new_approval_rule_approvals_required assert len(response.eligible_approvers) == len(new_approval_rule_user_ids) assert response.eligible_approvers[0]["id"] == new_approval_rule_user_ids[0] assert response.name == new_approval_rule_name def test_create_merge_request_approval_rule(project, resp_snippet): approval_rules = project.mergerequests.get(1).approval_rules data = { "name": new_approval_rule_name, "approvals_required": new_approval_rule_approvals_required, "rule_type": "regular", "user_ids": new_approval_rule_user_ids, "group_ids": group_ids, } response = approval_rules.create(data) assert response.approvals_required == new_approval_rule_approvals_required assert len(response.eligible_approvers) == len(new_approval_rule_user_ids) assert response.eligible_approvers[0]["id"] == new_approval_rule_user_ids[0] assert response.name == new_approval_rule_name def test_update_merge_request_approval_rule(project, resp_snippet): approval_rules = project.mergerequests.get(1).approval_rules ar_1 = approval_rules.list()[0] ar_1.user_ids = updated_approval_rule_user_ids ar_1.approvals_required = updated_approval_rule_approvals_required ar_1.save() assert ar_1.approvals_required == updated_approval_rule_approvals_required assert len(ar_1.eligible_approvers) == len(updated_approval_rule_user_ids) assert ar_1.eligible_approvers[0]["id"] == updated_approval_rule_user_ids[0] python-gitlab-2.10.1/tests/unit/objects/test_project_statistics.py000066400000000000000000000014671416141341200254640ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ce/api/project_statistics.html """ import pytest import responses from gitlab.v4.objects import ProjectAdditionalStatistics @pytest.fixture def resp_project_statistics(): content = {"fetches": {"total": 50, "days": [{"count": 10, "date": "2018-01-10"}]}} with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/statistics", json=content, content_type="application/json", status=200, ) yield rsps def test_project_additional_statistics(project, resp_project_statistics): statistics = project.additionalstatistics.get() assert isinstance(statistics, ProjectAdditionalStatistics) assert statistics.fetches["total"] == 50 python-gitlab-2.10.1/tests/unit/objects/test_projects.py000066400000000000000000000112321416141341200233640ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ce/api/projects.html """ import pytest import responses from gitlab.v4.objects import Project project_content = {"name": "name", "id": 1} import_content = { "id": 1, "name": "project", "import_status": "scheduled", } @pytest.fixture def resp_get_project(): with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1", json=project_content, content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_list_projects(): with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/projects", json=[project_content], content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_import_bitbucket_server(): with responses.RequestsMock() as rsps: rsps.add( method=responses.POST, url="http://localhost/api/v4/import/bitbucket_server", json=import_content, content_type="application/json", status=201, ) yield rsps def test_get_project(gl, resp_get_project): data = gl.projects.get(1) assert isinstance(data, Project) assert data.name == "name" assert data.id == 1 def test_list_projects(gl, resp_list_projects): projects = gl.projects.list() assert isinstance(projects[0], Project) assert projects[0].name == "name" def test_import_bitbucket_server(gl, resp_import_bitbucket_server): res = gl.projects.import_bitbucket_server( bitbucket_server_project="project", bitbucket_server_repo="repo", bitbucket_server_url="url", bitbucket_server_username="username", personal_access_token="token", new_name="new_name", target_namespace="namespace", ) assert res["id"] == 1 assert res["name"] == "project" assert res["import_status"] == "scheduled" @pytest.mark.skip(reason="missing test") def test_list_user_projects(gl): pass @pytest.mark.skip(reason="missing test") def test_list_user_starred_projects(gl): pass @pytest.mark.skip(reason="missing test") def test_list_project_users(gl): pass @pytest.mark.skip(reason="missing test") def test_create_project(gl): pass @pytest.mark.skip(reason="missing test") def test_create_user_project(gl): pass @pytest.mark.skip(reason="missing test") def test_update_project(gl): pass @pytest.mark.skip(reason="missing test") def test_fork_project(gl): pass @pytest.mark.skip(reason="missing test") def test_list_project_forks(gl): pass @pytest.mark.skip(reason="missing test") def test_star_project(gl): pass @pytest.mark.skip(reason="missing test") def test_unstar_project(gl): pass @pytest.mark.skip(reason="missing test") def test_list_project_starrers(gl): pass @pytest.mark.skip(reason="missing test") def test_get_project_languages(gl): pass @pytest.mark.skip(reason="missing test") def test_archive_project(gl): pass @pytest.mark.skip(reason="missing test") def test_unarchive_project(gl): pass @pytest.mark.skip(reason="missing test") def test_remove_project(gl): pass @pytest.mark.skip(reason="missing test") def test_restore_project(gl): pass @pytest.mark.skip(reason="missing test") def test_upload_file(gl): pass @pytest.mark.skip(reason="missing test") def test_share_project(gl): pass @pytest.mark.skip(reason="missing test") def test_delete_shared_project_link(gl): pass @pytest.mark.skip(reason="missing test") def test_create_forked_from_relationship(gl): pass @pytest.mark.skip(reason="missing test") def test_delete_forked_from_relationship(gl): pass @pytest.mark.skip(reason="missing test") def test_search_projects_by_name(gl): pass @pytest.mark.skip(reason="missing test") def test_project_housekeeping(gl): pass @pytest.mark.skip(reason="missing test") def test_get_project_push_rules(gl): pass @pytest.mark.skip(reason="missing test") def test_create_project_push_rule(gl): pass @pytest.mark.skip(reason="missing test") def test_update_project_push_rule(gl): pass @pytest.mark.skip(reason="missing test") def test_delete_project_push_rule(gl): pass @pytest.mark.skip(reason="missing test") def test_transfer_project(gl): pass @pytest.mark.skip(reason="missing test") def test_project_pull_mirror(gl): pass @pytest.mark.skip(reason="missing test") def test_project_snapshot(gl): pass @pytest.mark.skip(reason="missing test") def test_import_github(gl): pass python-gitlab-2.10.1/tests/unit/objects/test_releases.py000066400000000000000000000104221416141341200233360ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ee/api/releases/index.html https://docs.gitlab.com/ee/api/releases/links.html """ import re import pytest import responses from gitlab.v4.objects import ProjectReleaseLink tag_name = "v1.0.0" encoded_tag_name = "v1%2E0%2E0" release_name = "demo-release" release_description = "my-rel-desc" released_at = "2019-03-15T08:00:00Z" link_name = "hello-world" link_url = "https://gitlab.example.com/group/hello/-/jobs/688/artifacts/raw/bin/hello-darwin-amd64" direct_url = f"https://gitlab.example.com/group/hello/-/releases/{encoded_tag_name}/downloads/hello-world" new_link_type = "package" link_content = { "id": 2, "name": link_name, "url": link_url, "direct_asset_url": direct_url, "external": False, "link_type": "other", } release_content = { "id": 3, "tag_name": tag_name, "name": release_name, "description": release_description, "milestones": [], "released_at": released_at, } release_url = re.compile( rf"http://localhost/api/v4/projects/1/releases/{encoded_tag_name}" ) links_url = re.compile( rf"http://localhost/api/v4/projects/1/releases/{encoded_tag_name}/assets/links" ) link_id_url = re.compile( rf"http://localhost/api/v4/projects/1/releases/{encoded_tag_name}/assets/links/1" ) @pytest.fixture def resp_list_links(): with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url=links_url, json=[link_content], content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_get_link(): with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url=link_id_url, json=link_content, content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_create_link(): with responses.RequestsMock() as rsps: rsps.add( method=responses.POST, url=links_url, json=link_content, content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_update_link(): updated_content = dict(link_content) updated_content["link_type"] = new_link_type with responses.RequestsMock() as rsps: rsps.add( method=responses.PUT, url=link_id_url, json=updated_content, content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_delete_link(no_content): with responses.RequestsMock() as rsps: rsps.add( method=responses.DELETE, url=link_id_url, json=link_content, content_type="application/json", status=204, ) yield rsps @pytest.fixture def resp_update_release(): updated_content = dict(release_content) with responses.RequestsMock() as rsps: rsps.add( method=responses.PUT, url=release_url, json=updated_content, content_type="application/json", status=200, ) yield rsps def test_list_release_links(release, resp_list_links): links = release.links.list() assert isinstance(links, list) assert isinstance(links[0], ProjectReleaseLink) assert links[0].url == link_url def test_get_release_link(release, resp_get_link): link = release.links.get(1) assert isinstance(link, ProjectReleaseLink) assert link.url == link_url def test_create_release_link(release, resp_create_link): link = release.links.create({"url": link_url, "name": link_name}) assert isinstance(link, ProjectReleaseLink) assert link.url == link_url def test_update_release_link(release, resp_update_link): link = release.links.get(1, lazy=True) link.link_type = new_link_type link.save() assert link.link_type == new_link_type def test_delete_release_link(release, resp_delete_link): link = release.links.get(1, lazy=True) link.delete() def test_update_release(release, resp_update_release): release.name = release_name release.description = release_description release.save() assert release.name == release_name assert release.description == release_description python-gitlab-2.10.1/tests/unit/objects/test_remote_mirrors.py000066400000000000000000000043501416141341200246060ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ce/api/remote_mirrors.html """ import pytest import responses from gitlab.v4.objects import ProjectRemoteMirror @pytest.fixture def resp_remote_mirrors(): content = { "enabled": True, "id": 1, "last_error": None, "last_successful_update_at": "2020-01-06T17:32:02.823Z", "last_update_at": "2020-01-06T17:32:02.823Z", "last_update_started_at": "2020-01-06T17:31:55.864Z", "only_protected_branches": True, "update_status": "none", "url": "https://*****:*****@gitlab.com/gitlab-org/security/gitlab.git", } with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/remote_mirrors", json=[content], content_type="application/json", status=200, ) rsps.add( method=responses.POST, url="http://localhost/api/v4/projects/1/remote_mirrors", json=content, content_type="application/json", status=200, ) updated_content = dict(content) updated_content["update_status"] = "finished" rsps.add( method=responses.PUT, url="http://localhost/api/v4/projects/1/remote_mirrors/1", json=updated_content, content_type="application/json", status=200, ) yield rsps def test_list_project_remote_mirrors(project, resp_remote_mirrors): mirrors = project.remote_mirrors.list() assert isinstance(mirrors, list) assert isinstance(mirrors[0], ProjectRemoteMirror) assert mirrors[0].enabled def test_create_project_remote_mirror(project, resp_remote_mirrors): mirror = project.remote_mirrors.create({"url": "https://example.com"}) assert isinstance(mirror, ProjectRemoteMirror) assert mirror.update_status == "none" def test_update_project_remote_mirror(project, resp_remote_mirrors): mirror = project.remote_mirrors.create({"url": "https://example.com"}) mirror.only_protected_branches = True mirror.save() assert mirror.update_status == "finished" assert mirror.only_protected_branches python-gitlab-2.10.1/tests/unit/objects/test_repositories.py000066400000000000000000000026321416141341200242660ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ee/api/repositories.html https://docs.gitlab.com/ee/api/repository_files.html """ from urllib.parse import quote import pytest import responses from gitlab.v4.objects import ProjectFile file_path = "app/models/key.rb" ref = "main" @pytest.fixture def resp_get_repository_file(): file_response = { "file_name": "key.rb", "file_path": file_path, "size": 1476, "encoding": "base64", "content": "IyA9PSBTY2hlbWEgSW5mb3...", "content_sha256": "4c294617b60715c1d218e61164a3abd4808a4284cbc30e6728a01ad9aada4481", "ref": ref, "blob_id": "79f7bbd25901e8334750839545a9bd021f0e4c83", "commit_id": "d5a3ff139356ce33e37e73add446f16869741b50", "last_commit_id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d", } # requests also encodes `.` encoded_path = quote(file_path, safe="").replace(".", "%2E") with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url=f"http://localhost/api/v4/projects/1/repository/files/{encoded_path}", json=file_response, content_type="application/json", status=200, ) yield rsps def test_get_repository_file(project, resp_get_repository_file): file = project.files.get(file_path, ref=ref) assert isinstance(file, ProjectFile) assert file.file_path == file_path python-gitlab-2.10.1/tests/unit/objects/test_resource_label_events.py000066400000000000000000000065401416141341200261130ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ee/api/resource_label_events.html """ import pytest import responses from gitlab.v4.objects import ( GroupEpicResourceLabelEvent, ProjectIssueResourceLabelEvent, ProjectMergeRequestResourceLabelEvent, ) @pytest.fixture() def resp_group_epic_request_label_events(): epic_content = {"id": 1} events_content = {"id": 1, "resource_type": "Epic"} with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/groups/1/epics", json=[epic_content], content_type="application/json", status=200, ) rsps.add( method=responses.GET, url="http://localhost/api/v4/groups/1/epics/1/resource_label_events", json=[events_content], content_type="application/json", status=200, ) yield rsps @pytest.fixture() def resp_merge_request_label_events(): mr_content = {"iid": 1} events_content = {"id": 1, "resource_type": "MergeRequest"} with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/merge_requests", json=[mr_content], content_type="application/json", status=200, ) rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/merge_requests/1/resource_label_events", json=[events_content], content_type="application/json", status=200, ) yield rsps @pytest.fixture() def resp_project_issue_label_events(): issue_content = {"iid": 1} events_content = {"id": 1, "resource_type": "Issue"} with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/issues", json=[issue_content], content_type="application/json", status=200, ) rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/issues/1/resource_label_events", json=[events_content], content_type="application/json", status=200, ) yield rsps def test_project_issue_label_events(project, resp_project_issue_label_events): issue = project.issues.list()[0] label_events = issue.resourcelabelevents.list() assert isinstance(label_events, list) label_event = label_events[0] assert isinstance(label_event, ProjectIssueResourceLabelEvent) assert label_event.resource_type == "Issue" def test_merge_request_label_events(project, resp_merge_request_label_events): mr = project.mergerequests.list()[0] label_events = mr.resourcelabelevents.list() assert isinstance(label_events, list) label_event = label_events[0] assert isinstance(label_event, ProjectMergeRequestResourceLabelEvent) assert label_event.resource_type == "MergeRequest" def test_group_epic_request_label_events(group, resp_group_epic_request_label_events): epic = group.epics.list()[0] label_events = epic.resourcelabelevents.list() assert isinstance(label_events, list) label_event = label_events[0] assert isinstance(label_event, GroupEpicResourceLabelEvent) assert label_event.resource_type == "Epic" python-gitlab-2.10.1/tests/unit/objects/test_resource_milestone_events.py000066400000000000000000000046321416141341200270330ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ee/api/resource_milestone_events.html """ import pytest import responses from gitlab.v4.objects import ( ProjectIssueResourceMilestoneEvent, ProjectMergeRequestResourceMilestoneEvent, ) @pytest.fixture() def resp_merge_request_milestone_events(): mr_content = {"iid": 1} events_content = {"id": 1, "resource_type": "MergeRequest"} with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/merge_requests", json=[mr_content], content_type="application/json", status=200, ) rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/merge_requests/1/resource_milestone_events", json=[events_content], content_type="application/json", status=200, ) yield rsps @pytest.fixture() def resp_project_issue_milestone_events(): issue_content = {"iid": 1} events_content = {"id": 1, "resource_type": "Issue"} with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/issues", json=[issue_content], content_type="application/json", status=200, ) rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/issues/1/resource_milestone_events", json=[events_content], content_type="application/json", status=200, ) yield rsps def test_project_issue_milestone_events(project, resp_project_issue_milestone_events): issue = project.issues.list()[0] milestone_events = issue.resourcemilestoneevents.list() assert isinstance(milestone_events, list) milestone_event = milestone_events[0] assert isinstance(milestone_event, ProjectIssueResourceMilestoneEvent) assert milestone_event.resource_type == "Issue" def test_merge_request_milestone_events(project, resp_merge_request_milestone_events): mr = project.mergerequests.list()[0] milestone_events = mr.resourcemilestoneevents.list() assert isinstance(milestone_events, list) milestone_event = milestone_events[0] assert isinstance(milestone_event, ProjectMergeRequestResourceMilestoneEvent) assert milestone_event.resource_type == "MergeRequest" python-gitlab-2.10.1/tests/unit/objects/test_resource_state_events.py000066400000000000000000000061731416141341200261560ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ee/api/resource_state_events.html """ import pytest import responses from gitlab.v4.objects import ( ProjectIssueResourceStateEvent, ProjectMergeRequestResourceStateEvent, ) issue_event_content = {"id": 1, "resource_type": "Issue"} mr_event_content = {"id": 1, "resource_type": "MergeRequest"} @pytest.fixture() def resp_list_project_issue_state_events(): with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/issues/1/resource_state_events", json=[issue_event_content], content_type="application/json", status=200, ) yield rsps @pytest.fixture() def resp_get_project_issue_state_event(): with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/issues/1/resource_state_events/1", json=issue_event_content, content_type="application/json", status=200, ) yield rsps @pytest.fixture() def resp_list_merge_request_state_events(): with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/merge_requests/1/resource_state_events", json=[mr_event_content], content_type="application/json", status=200, ) yield rsps @pytest.fixture() def resp_get_merge_request_state_event(): with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/merge_requests/1/resource_state_events/1", json=mr_event_content, content_type="application/json", status=200, ) yield rsps def test_list_project_issue_state_events( project_issue, resp_list_project_issue_state_events ): state_events = project_issue.resourcestateevents.list() assert isinstance(state_events, list) state_event = state_events[0] assert isinstance(state_event, ProjectIssueResourceStateEvent) assert state_event.resource_type == "Issue" def test_get_project_issue_state_event( project_issue, resp_get_project_issue_state_event ): state_event = project_issue.resourcestateevents.get(1) assert isinstance(state_event, ProjectIssueResourceStateEvent) assert state_event.resource_type == "Issue" def test_list_merge_request_state_events( project_merge_request, resp_list_merge_request_state_events ): state_events = project_merge_request.resourcestateevents.list() assert isinstance(state_events, list) state_event = state_events[0] assert isinstance(state_event, ProjectMergeRequestResourceStateEvent) assert state_event.resource_type == "MergeRequest" def test_get_merge_request_state_event( project_merge_request, resp_get_merge_request_state_event ): state_event = project_merge_request.resourcestateevents.get(1) assert isinstance(state_event, ProjectMergeRequestResourceStateEvent) assert state_event.resource_type == "MergeRequest" python-gitlab-2.10.1/tests/unit/objects/test_runners.py000066400000000000000000000167601416141341200232420ustar00rootroot00000000000000import re import pytest import responses import gitlab runner_detail = { "active": True, "architecture": "amd64", "description": "test-1-20150125", "id": 6, "ip_address": "127.0.0.1", "is_shared": False, "contacted_at": "2016-01-25T16:39:48.066Z", "name": "test-runner", "online": True, "status": "online", "platform": "linux", "projects": [ { "id": 1, "name": "GitLab Community Edition", "name_with_namespace": "GitLab.org / GitLab Community Edition", "path": "gitlab-foss", "path_with_namespace": "gitlab-org/gitlab-foss", } ], "revision": "5nj35", "tag_list": ["ruby", "mysql"], "version": "v13.0.0", "access_level": "ref_protected", "maximum_timeout": 3600, } runner_shortinfo = { "active": True, "description": "test-1-20150125", "id": 6, "is_shared": False, "ip_address": "127.0.0.1", "name": "test-name", "online": True, "status": "online", } runner_jobs = [ { "id": 6, "ip_address": "127.0.0.1", "status": "running", "stage": "test", "name": "test", "ref": "master", "tag": False, "coverage": "99%", "created_at": "2017-11-16T08:50:29.000Z", "started_at": "2017-11-16T08:51:29.000Z", "finished_at": "2017-11-16T08:53:29.000Z", "duration": 120, "user": { "id": 1, "name": "John Doe2", "username": "user2", "state": "active", "avatar_url": "http://www.gravatar.com/avatar/c922747a93b40d1ea88262bf1aebee62?s=80&d=identicon", "web_url": "http://localhost/user2", "created_at": "2017-11-16T18:38:46.000Z", "bio": None, "location": None, "public_email": "", "skype": "", "linkedin": "", "twitter": "", "website_url": "", "organization": None, }, } ] @pytest.fixture def resp_get_runners_jobs(): with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/runners/6/jobs", json=runner_jobs, content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_get_runners_list(): with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url=re.compile(r".*?(/runners(/all)?|/(groups|projects)/1/runners)"), json=[runner_shortinfo], content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_runner_detail(): with responses.RequestsMock() as rsps: pattern = re.compile(r".*?/runners/6") rsps.add( method=responses.GET, url=pattern, json=runner_detail, content_type="application/json", status=200, ) rsps.add( method=responses.PUT, url=pattern, json=runner_detail, content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_runner_register(): with responses.RequestsMock() as rsps: pattern = re.compile(r".*?/runners") rsps.add( method=responses.POST, url=pattern, json={"id": "6", "token": "6337ff461c94fd3fa32ba3b1ff4125"}, content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_runner_enable(): with responses.RequestsMock() as rsps: pattern = re.compile(r".*?(projects|groups)/1/runners") rsps.add( method=responses.POST, url=pattern, json=runner_shortinfo, content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_runner_delete(): with responses.RequestsMock() as rsps: pattern = re.compile(r".*?/runners/6") rsps.add( method=responses.GET, url=pattern, json=runner_detail, content_type="application/json", status=200, ) rsps.add( method=responses.DELETE, url=pattern, status=204, ) yield rsps @pytest.fixture def resp_runner_disable(): with responses.RequestsMock() as rsps: pattern = re.compile(r".*?/(groups|projects)/1/runners/6") rsps.add( method=responses.DELETE, url=pattern, status=204, ) yield rsps @pytest.fixture def resp_runner_verify(): with responses.RequestsMock() as rsps: pattern = re.compile(r".*?/runners/verify") rsps.add( method=responses.POST, url=pattern, status=200, ) yield rsps def test_owned_runners_list(gl: gitlab.Gitlab, resp_get_runners_list): runners = gl.runners.list() assert runners[0].active is True assert runners[0].id == 6 assert runners[0].name == "test-name" assert len(runners) == 1 def test_project_runners_list(gl: gitlab.Gitlab, resp_get_runners_list): runners = gl.projects.get(1, lazy=True).runners.list() assert runners[0].active is True assert runners[0].id == 6 assert runners[0].name == "test-name" assert len(runners) == 1 def test_group_runners_list(gl: gitlab.Gitlab, resp_get_runners_list): runners = gl.groups.get(1, lazy=True).runners.list() assert runners[0].active is True assert runners[0].id == 6 assert runners[0].name == "test-name" assert len(runners) == 1 def test_all_runners_list(gl: gitlab.Gitlab, resp_get_runners_list): runners = gl.runners.all() assert runners[0].active is True assert runners[0].id == 6 assert runners[0].name == "test-name" assert len(runners) == 1 def test_create_runner(gl: gitlab.Gitlab, resp_runner_register): runner = gl.runners.create({"token": "token"}) assert runner.id == "6" assert runner.token == "6337ff461c94fd3fa32ba3b1ff4125" def test_get_update_runner(gl: gitlab.Gitlab, resp_runner_detail): runner = gl.runners.get(6) assert runner.active is True runner.tag_list.append("new") runner.save() def test_remove_runner(gl: gitlab.Gitlab, resp_runner_delete): runner = gl.runners.get(6) runner.delete() gl.runners.delete(6) def test_disable_project_runner(gl: gitlab.Gitlab, resp_runner_disable): gl.projects.get(1, lazy=True).runners.delete(6) def test_disable_group_runner(gl: gitlab.Gitlab, resp_runner_disable): gl.groups.get(1, lazy=True).runners.delete(6) def test_enable_project_runner(gl: gitlab.Gitlab, resp_runner_enable): runner = gl.projects.get(1, lazy=True).runners.create({"runner_id": 6}) assert runner.active is True assert runner.id == 6 assert runner.name == "test-name" def test_enable_group_runner(gl: gitlab.Gitlab, resp_runner_enable): runner = gl.groups.get(1, lazy=True).runners.create({"runner_id": 6}) assert runner.active is True assert runner.id == 6 assert runner.name == "test-name" def test_verify_runner(gl: gitlab.Gitlab, resp_runner_verify): gl.runners.verify("token") def test_runner_jobs(gl: gitlab.Gitlab, resp_get_runners_jobs): jobs = gl.runners.get(6, lazy=True).jobs.list() assert jobs[0].duration == 120 assert jobs[0].name == "test" assert jobs[0].user.get("name") == "John Doe2" assert len(jobs) == 1 python-gitlab-2.10.1/tests/unit/objects/test_services.py000066400000000000000000000053541416141341200233660ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ce/api/services.html """ import pytest import responses from gitlab.v4.objects import ProjectService @pytest.fixture def resp_service(): content = { "id": 100152, "title": "Pipelines emails", "slug": "pipelines-email", "created_at": "2019-01-14T08:46:43.637+01:00", "updated_at": "2019-07-01T14:10:36.156+02:00", "active": True, "commit_events": True, "push_events": True, "issues_events": True, "confidential_issues_events": True, "merge_requests_events": True, "tag_push_events": True, "note_events": True, "confidential_note_events": True, "pipeline_events": True, "wiki_page_events": True, "job_events": True, "comment_on_event_enabled": True, "project_id": 1, } with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/services", json=[content], content_type="application/json", status=200, ) rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/services", json=content, content_type="application/json", status=200, ) rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/services/pipelines-email", json=content, content_type="application/json", status=200, ) updated_content = dict(content) updated_content["issues_events"] = False rsps.add( method=responses.PUT, url="http://localhost/api/v4/projects/1/services/pipelines-email", json=updated_content, content_type="application/json", status=200, ) yield rsps def test_list_active_services(project, resp_service): services = project.services.list() assert isinstance(services, list) assert isinstance(services[0], ProjectService) assert services[0].active assert services[0].push_events def test_list_available_services(project, resp_service): services = project.services.available() assert isinstance(services, list) assert isinstance(services[0], str) def test_get_service(project, resp_service): service = project.services.get("pipelines-email") assert isinstance(service, ProjectService) assert service.push_events is True def test_update_service(project, resp_service): service = project.services.get("pipelines-email") service.issues_events = False service.save() assert service.issues_events is False python-gitlab-2.10.1/tests/unit/objects/test_snippets.py000066400000000000000000000047021416141341200234040ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ce/api/project_snippets.html https://docs.gitlab.com/ee/api/snippets.html (todo) """ import pytest import responses title = "Example Snippet Title" visibility = "private" new_title = "new-title" @pytest.fixture def resp_snippet(): content = { "title": title, "description": "More verbose snippet description", "file_name": "example.txt", "content": "source code with multiple lines", "visibility": visibility, } with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/snippets", json=[content], content_type="application/json", status=200, ) rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/snippets/1", json=content, content_type="application/json", status=200, ) rsps.add( method=responses.POST, url="http://localhost/api/v4/projects/1/snippets", json=content, content_type="application/json", status=200, ) updated_content = dict(content) updated_content["title"] = new_title updated_content["visibility"] = visibility rsps.add( method=responses.PUT, url="http://localhost/api/v4/projects/1/snippets", json=updated_content, content_type="application/json", status=200, ) yield rsps def test_list_project_snippets(project, resp_snippet): snippets = project.snippets.list() assert len(snippets) == 1 assert snippets[0].title == title assert snippets[0].visibility == visibility def test_get_project_snippet(project, resp_snippet): snippet = project.snippets.get(1) assert snippet.title == title assert snippet.visibility == visibility def test_create_update_project_snippets(project, resp_snippet): snippet = project.snippets.create( { "title": title, "file_name": title, "content": title, "visibility": visibility, } ) assert snippet.title == title assert snippet.visibility == visibility snippet.title = new_title snippet.save() assert snippet.title == new_title assert snippet.visibility == visibility python-gitlab-2.10.1/tests/unit/objects/test_submodules.py000066400000000000000000000026601416141341200237220ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ce/api/repository_submodules.html """ import pytest import responses @pytest.fixture def resp_update_submodule(): 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": None, } with responses.RequestsMock() as rsps: rsps.add( method=responses.PUT, url="http://localhost/api/v4/projects/1/repository/submodules/foo%2Fbar", json=content, content_type="application/json", status=200, ) yield rsps def test_update_submodule(project, resp_update_submodule): ret = project.update_submodule( submodule="foo/bar", branch="master", commit_sha="4c3674f66071e30b3311dac9b9ccc90502a72664", commit_message="Message", ) assert isinstance(ret, dict) assert ret["message"] == "Message" assert ret["id"] == "ed899a2f4b50b4370feeea94676502b42383c746" python-gitlab-2.10.1/tests/unit/objects/test_todos.py000066400000000000000000000030231416141341200226620ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ce/api/todos.html """ import json import os import pytest import responses from gitlab.v4.objects import Todo with open(os.path.dirname(__file__) + "/../data/todo.json", "r") as json_file: todo_content = json_file.read() json_content = json.loads(todo_content) @pytest.fixture def resp_todo(): with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/todos", json=json_content, content_type="application/json", status=200, ) rsps.add( method=responses.POST, url="http://localhost/api/v4/todos/102/mark_as_done", json=json_content[0], content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_mark_all_as_done(): with responses.RequestsMock() as rsps: rsps.add( method=responses.POST, url="http://localhost/api/v4/todos/mark_as_done", json={}, content_type="application/json", status=204, ) yield rsps def test_todo(gl, resp_todo): todo = gl.todos.list()[0] assert isinstance(todo, Todo) assert todo.id == 102 assert todo.target_type == "MergeRequest" assert todo.target["assignee"]["username"] == "root" todo.mark_as_done() def test_todo_mark_all_as_done(gl, resp_mark_all_as_done): gl.todos.mark_all_as_done() python-gitlab-2.10.1/tests/unit/objects/test_users.py000066400000000000000000000133411416141341200226770ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ce/api/users.html """ import pytest import responses from gitlab.v4.objects import User, UserMembership, UserStatus @pytest.fixture def resp_get_user(): content = { "name": "name", "id": 1, "password": "password", "username": "username", "email": "email", } with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/users/1", json=content, content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_get_user_memberships(): content = [ { "source_id": 1, "source_name": "Project one", "source_type": "Project", "access_level": "20", }, { "source_id": 3, "source_name": "Group three", "source_type": "Namespace", "access_level": "20", }, ] with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/users/1/memberships", json=content, content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_activate(): with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: rsps.add( method=responses.POST, url="http://localhost/api/v4/users/1/activate", json={}, content_type="application/json", status=201, ) rsps.add( method=responses.POST, url="http://localhost/api/v4/users/1/deactivate", json={}, content_type="application/json", status=201, ) yield rsps @pytest.fixture def resp_get_user_status(): content = { "message": "test", "message_html": "

Message

", "emoji": "thumbsup", } with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/users/1/status", json=content, content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_delete_user_identity(no_content): with responses.RequestsMock() as rsps: rsps.add( method=responses.DELETE, url="http://localhost/api/v4/users/1/identities/test_provider", json=no_content, content_type="application/json", status=204, ) yield rsps @pytest.fixture def resp_follow_unfollow(): user = { "id": 1, "username": "john_smith", "name": "John Smith", "state": "active", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg", "web_url": "http://localhost:3000/john_smith", } with responses.RequestsMock() as rsps: rsps.add( method=responses.POST, url="http://localhost/api/v4/users/1/follow", json=user, content_type="application/json", status=201, ) rsps.add( method=responses.POST, url="http://localhost/api/v4/users/1/unfollow", json=user, content_type="application/json", status=201, ) yield rsps @pytest.fixture def resp_followers_following(): content = [ { "id": 2, "name": "Lennie Donnelly", "username": "evette.kilback", "state": "active", "avatar_url": "https://www.gravatar.com/avatar/7955171a55ac4997ed81e5976287890a?s=80&d=identicon", "web_url": "http://127.0.0.1:3000/evette.kilback", }, { "id": 4, "name": "Serena Bradtke", "username": "cammy", "state": "active", "avatar_url": "https://www.gravatar.com/avatar/a2daad869a7b60d3090b7b9bef4baf57?s=80&d=identicon", "web_url": "http://127.0.0.1:3000/cammy", }, ] with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url="http://localhost/api/v4/users/1/followers", json=content, content_type="application/json", status=200, ) rsps.add( method=responses.GET, url="http://localhost/api/v4/users/1/following", json=content, content_type="application/json", status=200, ) yield rsps def test_get_user(gl, resp_get_user): user = gl.users.get(1) assert isinstance(user, User) assert user.name == "name" assert user.id == 1 def test_user_memberships(user, resp_get_user_memberships): memberships = user.memberships.list() assert isinstance(memberships[0], UserMembership) assert memberships[0].source_type == "Project" def test_user_status(user, resp_get_user_status): status = user.status.get() assert isinstance(status, UserStatus) assert status.message == "test" assert status.emoji == "thumbsup" def test_user_activate_deactivate(user, resp_activate): user.activate() user.deactivate() def test_delete_user_identity(user, resp_delete_user_identity): user.identityproviders.delete("test_provider") def test_user_follow_unfollow(user, resp_follow_unfollow): user.follow() user.unfollow() def test_list_followers(user, resp_followers_following): followers = user.followers_users.list() followings = user.following_users.list() assert isinstance(followers[0], User) assert followers[0].id == 2 assert isinstance(followings[0], User) assert followings[1].id == 4 python-gitlab-2.10.1/tests/unit/objects/test_variables.py000066400000000000000000000122111416141341200235010ustar00rootroot00000000000000""" GitLab API: https://docs.gitlab.com/ee/api/instance_level_ci_variables.html https://docs.gitlab.com/ee/api/project_level_variables.html https://docs.gitlab.com/ee/api/group_level_variables.html """ import re import pytest import responses from gitlab.v4.objects import GroupVariable, ProjectVariable, Variable key = "TEST_VARIABLE_1" value = "TEST_1" new_value = "TEST_2" variable_content = { "key": key, "variable_type": "env_var", "value": value, "protected": False, "masked": True, } variables_url = re.compile( r"http://localhost/api/v4/(((groups|projects)/1)|(admin/ci))/variables" ) variables_key_url = re.compile( rf"http://localhost/api/v4/(((groups|projects)/1)|(admin/ci))/variables/{key}" ) @pytest.fixture def resp_list_variables(): with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url=variables_url, json=[variable_content], content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_get_variable(): with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, url=variables_key_url, json=variable_content, content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_create_variable(): with responses.RequestsMock() as rsps: rsps.add( method=responses.POST, url=variables_url, json=variable_content, content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_update_variable(): updated_content = dict(variable_content) updated_content["value"] = new_value with responses.RequestsMock() as rsps: rsps.add( method=responses.PUT, url=variables_key_url, json=updated_content, content_type="application/json", status=200, ) yield rsps @pytest.fixture def resp_delete_variable(no_content): with responses.RequestsMock() as rsps: rsps.add( method=responses.DELETE, url=variables_key_url, json=no_content, content_type="application/json", status=204, ) yield rsps def test_list_instance_variables(gl, resp_list_variables): variables = gl.variables.list() assert isinstance(variables, list) assert isinstance(variables[0], Variable) assert variables[0].value == value def test_get_instance_variable(gl, resp_get_variable): variable = gl.variables.get(key) assert isinstance(variable, Variable) assert variable.value == value def test_create_instance_variable(gl, resp_create_variable): variable = gl.variables.create({"key": key, "value": value}) assert isinstance(variable, Variable) assert variable.value == value def test_update_instance_variable(gl, resp_update_variable): variable = gl.variables.get(key, lazy=True) variable.value = new_value variable.save() assert variable.value == new_value def test_delete_instance_variable(gl, resp_delete_variable): variable = gl.variables.get(key, lazy=True) variable.delete() def test_list_project_variables(project, resp_list_variables): variables = project.variables.list() assert isinstance(variables, list) assert isinstance(variables[0], ProjectVariable) assert variables[0].value == value def test_get_project_variable(project, resp_get_variable): variable = project.variables.get(key) assert isinstance(variable, ProjectVariable) assert variable.value == value def test_create_project_variable(project, resp_create_variable): variable = project.variables.create({"key": key, "value": value}) assert isinstance(variable, ProjectVariable) assert variable.value == value def test_update_project_variable(project, resp_update_variable): variable = project.variables.get(key, lazy=True) variable.value = new_value variable.save() assert variable.value == new_value def test_delete_project_variable(project, resp_delete_variable): variable = project.variables.get(key, lazy=True) variable.delete() def test_list_group_variables(group, resp_list_variables): variables = group.variables.list() assert isinstance(variables, list) assert isinstance(variables[0], GroupVariable) assert variables[0].value == value def test_get_group_variable(group, resp_get_variable): variable = group.variables.get(key) assert isinstance(variable, GroupVariable) assert variable.value == value def test_create_group_variable(group, resp_create_variable): variable = group.variables.create({"key": key, "value": value}) assert isinstance(variable, GroupVariable) assert variable.value == value def test_update_group_variable(group, resp_update_variable): variable = group.variables.get(key, lazy=True) variable.value = new_value variable.save() assert variable.value == new_value def test_delete_group_variable(group, resp_delete_variable): variable = group.variables.get(key, lazy=True) variable.delete() python-gitlab-2.10.1/tests/unit/test_base.py000066400000000000000000000130351416141341200210170ustar00rootroot00000000000000# -*- 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 pytest import gitlab from gitlab import base class FakeGitlab(object): pass class FakeObject(base.RESTObject): pass class FakeManager(base.RESTManager): _obj_cls = FakeObject _path = "/tests" @pytest.fixture def fake_gitlab(): return FakeGitlab() @pytest.fixture def fake_manager(fake_gitlab): return FakeManager(fake_gitlab) class TestRESTManager: def test_computed_path_simple(self): class MGR(base.RESTManager): _path = "/tests" _obj_cls = object mgr = MGR(FakeGitlab()) assert 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()) assert mgr._computed_path == "/tests/42/cases" def test_path_property(self): class MGR(base.RESTManager): _path = "/tests" _obj_cls = object mgr = MGR(FakeGitlab()) assert mgr.path == "/tests" class TestRESTObject: def test_instantiate(self, fake_gitlab, fake_manager): obj = FakeObject(fake_manager, {"foo": "bar"}) assert {"foo": "bar"} == obj._attrs assert {} == obj._updated_attrs assert obj._create_managers() is None assert fake_manager == obj.manager assert fake_gitlab == obj.manager.gitlab def test_instantiate_non_dict(self, fake_gitlab, fake_manager): with pytest.raises(gitlab.exceptions.GitlabParsingError): FakeObject(fake_manager, ["a", "list", "fails"]) def test_picklability(self, fake_manager): obj = FakeObject(fake_manager, {"foo": "bar"}) original_obj_module = obj._module pickled = pickle.dumps(obj) unpickled = pickle.loads(pickled) assert isinstance(unpickled, FakeObject) assert hasattr(unpickled, "_module") assert unpickled._module == original_obj_module pickle.dumps(unpickled) def test_attrs(self, fake_manager): obj = FakeObject(fake_manager, {"foo": "bar"}) assert "bar" == obj.foo with pytest.raises(AttributeError): getattr(obj, "bar") obj.bar = "baz" assert "baz" == obj.bar assert {"foo": "bar"} == obj._attrs assert {"bar": "baz"} == obj._updated_attrs def test_get_id(self, fake_manager): obj = FakeObject(fake_manager, {"foo": "bar"}) obj.id = 42 assert 42 == obj.get_id() obj.id = None assert obj.get_id() is None def test_custom_id_attr(self, fake_manager): class OtherFakeObject(FakeObject): _id_attr = "foo" obj = OtherFakeObject(fake_manager, {"foo": "bar"}) assert "bar" == obj.get_id() def test_update_attrs(self, fake_manager): obj = FakeObject(fake_manager, {"foo": "bar"}) obj.bar = "baz" obj._update_attrs({"foo": "foo", "bar": "bar"}) assert {"foo": "foo", "bar": "bar"} == obj._attrs assert {} == obj._updated_attrs def test_update_attrs_deleted(self, fake_manager): obj = FakeObject(fake_manager, {"foo": "foo", "bar": "bar"}) obj.bar = "baz" obj._update_attrs({"foo": "foo"}) assert {"foo": "foo"} == obj._attrs assert {} == obj._updated_attrs def test_dir_unique(self, fake_manager): obj = FakeObject(fake_manager, {"manager": "foo"}) assert len(dir(obj)) == len(set(dir(obj))) def test_create_managers(self, fake_gitlab, fake_manager): class ObjectWithManager(FakeObject): _managers = (("fakes", "FakeManager"),) obj = ObjectWithManager(fake_manager, {"foo": "bar"}) obj.id = 42 assert isinstance(obj.fakes, FakeManager) assert obj.fakes.gitlab == fake_gitlab assert obj.fakes._parent == obj def test_equality(self, fake_manager): obj1 = FakeObject(fake_manager, {"id": "foo"}) obj2 = FakeObject(fake_manager, {"id": "foo", "other_attr": "bar"}) assert obj1 == obj2 def test_equality_custom_id(self, fake_manager): class OtherFakeObject(FakeObject): _id_attr = "foo" obj1 = OtherFakeObject(fake_manager, {"foo": "bar"}) obj2 = OtherFakeObject(fake_manager, {"foo": "bar", "other_attr": "baz"}) assert obj1 == obj2 def test_inequality(self, fake_manager): obj1 = FakeObject(fake_manager, {"id": "foo"}) obj2 = FakeObject(fake_manager, {"id": "bar"}) assert obj1 != obj2 def test_inequality_no_id(self, fake_manager): obj1 = FakeObject(fake_manager, {"attr1": "foo"}) obj2 = FakeObject(fake_manager, {"attr1": "bar"}) assert obj1 != obj2 python-gitlab-2.10.1/tests/unit/test_cli.py000066400000000000000000000110571416141341200206560ustar00rootroot00000000000000#!/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 io import os import tempfile from contextlib import redirect_stderr # noqa: H302 import pytest from gitlab import cli @pytest.mark.parametrize( "what,expected_class", [ ("class", "Class"), ("test-class", "TestClass"), ("test-longer-class", "TestLongerClass"), ("current-user-gpg-key", "CurrentUserGPGKey"), ("user-gpg-key", "UserGPGKey"), ("ldap-group", "LDAPGroup"), ], ) def test_what_to_cls(what, expected_class): def _namespace(): pass ExpectedClass = type(expected_class, (), {}) _namespace.__dict__[expected_class] = ExpectedClass assert cli.what_to_cls(what, _namespace) == ExpectedClass @pytest.mark.parametrize( "class_name,expected_what", [ ("Class", "class"), ("TestClass", "test-class"), ("TestUPPERCASEClass", "test-uppercase-class"), ("UPPERCASETestClass", "uppercase-test-class"), ("CurrentUserGPGKey", "current-user-gpg-key"), ("UserGPGKey", "user-gpg-key"), ("LDAPGroup", "ldap-group"), ], ) def test_cls_to_what(class_name, expected_what): TestClass = type(class_name, (), {}) assert cli.cls_to_what(TestClass) == expected_what def test_die(): fl = io.StringIO() with redirect_stderr(fl): with pytest.raises(SystemExit) as test: cli.die("foobar") assert fl.getvalue() == "foobar\n" assert test.value.code == 1 def test_parse_value(): ret = cli._parse_value("foobar") assert ret == "foobar" ret = cli._parse_value(True) assert ret is True ret = cli._parse_value(1) assert ret == 1 ret = cli._parse_value(None) assert ret is None fd, temp_path = tempfile.mkstemp() os.write(fd, b"content") os.close(fd) ret = cli._parse_value("@%s" % temp_path) assert ret == "content" os.unlink(temp_path) fl = io.StringIO() with redirect_stderr(fl): with pytest.raises(SystemExit) as exc: cli._parse_value("@/thisfileprobablydoesntexist") assert ( fl.getvalue() == "[Errno 2] No such file or directory:" " '/thisfileprobablydoesntexist'\n" ) assert exc.value.code == 1 def test_base_parser(): parser = cli._get_base_parser() args = parser.parse_args(["-v", "-g", "gl_id", "-c", "foo.cfg", "-c", "bar.cfg"]) assert args.verbose assert args.gitlab == "gl_id" assert args.config_file == ["foo.cfg", "bar.cfg"] def test_v4_parse_args(): parser = cli._get_parser() args = parser.parse_args(["project", "list"]) assert args.what == "project" assert args.whaction == "list" def test_v4_parser(): parser = cli._get_parser() subparsers = next( action for action in parser._actions if isinstance(action, argparse._SubParsersAction) ) assert subparsers is not None assert "project" in subparsers.choices user_subparsers = next( action for action in subparsers.choices["project"]._actions if isinstance(action, argparse._SubParsersAction) ) assert user_subparsers is not None assert "list" in user_subparsers.choices assert "get" in user_subparsers.choices assert "delete" in user_subparsers.choices assert "update" in user_subparsers.choices assert "create" in user_subparsers.choices assert "archive" in user_subparsers.choices assert "unarchive" in user_subparsers.choices actions = user_subparsers.choices["create"]._option_string_actions assert not 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 assert actions["--name"].required python-gitlab-2.10.1/tests/unit/test_config.py000066400000000000000000000145661416141341200213640ustar00rootroot00000000000000# -*- 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 io import os from textwrap import dedent import mock import pytest from gitlab import config, USER_AGENT custom_user_agent = "my-package/1.0.0" 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 """ custom_user_agent_config = """[global] default = one user_agent = {} [one] url = http://one.url private_token = ABCDEF """.format( custom_user_agent ) 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 """ @mock.patch.dict(os.environ, {"PYTHON_GITLAB_CFG": "/some/path"}) def test_env_config_present(): assert ["/some/path"] == config._env_config() @mock.patch.dict(os.environ, {}, clear=True) def test_env_config_missing(): assert [] == config._env_config() @mock.patch("os.path.exists") def test_missing_config(path_exists): path_exists.return_value = False with pytest.raises(config.GitlabConfigMissingError): config.GitlabConfigParser("test") @mock.patch("os.path.exists") @mock.patch("builtins.open") def test_invalid_id(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") with pytest.raises(config.GitlabIDError): config.GitlabConfigParser() fd = io.StringIO(valid_config) fd.close = mock.Mock(return_value=None) m_open.return_value = fd with pytest.raises(config.GitlabDataError): config.GitlabConfigParser(gitlab_id="not_there") @mock.patch("os.path.exists") @mock.patch("builtins.open") def test_invalid_data(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") with pytest.raises(config.GitlabDataError): config.GitlabConfigParser(gitlab_id="two") with pytest.raises(config.GitlabDataError): config.GitlabConfigParser(gitlab_id="three") with pytest.raises(config.GitlabDataError) as emgr: config.GitlabConfigParser("four") assert "Unsupported per_page number: 200" == emgr.value.args[0] @mock.patch("os.path.exists") @mock.patch("builtins.open") def test_valid_data(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() assert "one" == cp.gitlab_id assert "http://one.url" == cp.url assert "ABCDEF" == cp.private_token assert cp.oauth_token is None assert 2 == cp.timeout assert cp.ssl_verify is True assert cp.per_page is None fd = io.StringIO(valid_config) fd.close = mock.Mock(return_value=None) m_open.return_value = fd cp = config.GitlabConfigParser(gitlab_id="two") assert "two" == cp.gitlab_id assert "https://two.url" == cp.url assert "GHIJKL" == cp.private_token assert cp.oauth_token is None assert 10 == cp.timeout assert cp.ssl_verify is False fd = io.StringIO(valid_config) fd.close = mock.Mock(return_value=None) m_open.return_value = fd cp = config.GitlabConfigParser(gitlab_id="three") assert "three" == cp.gitlab_id assert "https://three.url" == cp.url assert "MNOPQR" == cp.private_token assert cp.oauth_token is None assert 2 == cp.timeout assert "/path/to/CA/bundle.crt" == cp.ssl_verify assert 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") assert "four" == cp.gitlab_id assert "https://four.url" == cp.url assert cp.private_token is None assert "STUV" == cp.oauth_token assert 2 == cp.timeout assert cp.ssl_verify is True @mock.patch("os.path.exists") @mock.patch("builtins.open") def test_data_from_helper(m_open, path_exists, tmp_path): helper = tmp_path / "helper.sh" helper.write_text( dedent( """\ #!/bin/sh echo "secret" """ ) ) helper.chmod(0o755) fd = io.StringIO( dedent( """\ [global] default = helper [helper] url = https://helper.url oauth_token = helper: %s """ ) % helper ) fd.close = mock.Mock(return_value=None) m_open.return_value = fd cp = config.GitlabConfigParser(gitlab_id="helper") assert "helper" == cp.gitlab_id assert "https://helper.url" == cp.url assert cp.private_token is None assert "secret" == cp.oauth_token @mock.patch("os.path.exists") @mock.patch("builtins.open") @pytest.mark.parametrize( "config_string,expected_agent", [ (valid_config, USER_AGENT), (custom_user_agent_config, custom_user_agent), ], ) def test_config_user_agent(m_open, path_exists, config_string, expected_agent): fd = io.StringIO(config_string) fd.close = mock.Mock(return_value=None) m_open.return_value = fd cp = config.GitlabConfigParser() assert cp.user_agent == expected_agent python-gitlab-2.10.1/tests/unit/test_exceptions.py000066400000000000000000000007631416141341200222720ustar00rootroot00000000000000import pytest from gitlab import exceptions def test_error_raises_from_http_error(): """Methods decorated with @on_http_error should raise from GitlabHttpError.""" class TestError(Exception): pass @exceptions.on_http_error(TestError) def raise_error_from_http_error(): raise exceptions.GitlabHttpError with pytest.raises(TestError) as context: raise_error_from_http_error() assert isinstance(context.value.__cause__, exceptions.GitlabHttpError) python-gitlab-2.10.1/tests/unit/test_gitlab.py000066400000000000000000000106511416141341200213500ustar00rootroot00000000000000# -*- 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 pickle import pytest from httmock import HTTMock, response, urlmatch, with_httmock # noqa from gitlab import Gitlab, GitlabList, USER_AGENT from gitlab.v4.objects import CurrentUser username = "username" user_id = 1 @urlmatch(scheme="http", netloc="localhost", path="/api/v4/user", method="get") def resp_get_user(url, request): headers = {"content-type": "application/json"} content = '{{"id": {0:d}, "username": "{1:s}"}}'.format(user_id, username).encode( "utf-8" ) return response(200, content, headers, None, 5, request) @urlmatch(scheme="http", netloc="localhost", path="/api/v4/tests", method="get") def resp_page_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_page_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) def test_gitlab_build_list(gl): with HTTMock(resp_page_1): obj = gl.http_list("/tests", as_list=False) assert len(obj) == 2 assert obj._next_url == "http://localhost/api/v4/tests?per_page=1&page=2" assert obj.current_page == 1 assert obj.prev_page is None assert obj.next_page == 2 assert obj.per_page == 1 assert obj.total_pages == 2 assert obj.total == 2 with HTTMock(resp_page_2): test_list = list(obj) assert len(test_list) == 2 assert test_list[0]["a"] == "b" assert test_list[1]["c"] == "d" @with_httmock(resp_page_1, resp_page_2) def test_gitlab_all_omitted_when_as_list(gl): result = gl.http_list("/tests", as_list=False, all=True) assert isinstance(result, GitlabList) def test_gitlab_strip_base_url(gl_trailing): assert gl_trailing.url == "http://localhost" def test_gitlab_strip_api_url(gl_trailing): assert gl_trailing.api_url == "http://localhost/api/v4" def test_gitlab_build_url(gl_trailing): r = gl_trailing._build_url("/projects") assert r == "http://localhost/api/v4/projects" def test_gitlab_pickability(gl): original_gl_objects = gl._objects pickled = pickle.dumps(gl) unpickled = pickle.loads(pickled) assert isinstance(unpickled, Gitlab) assert hasattr(unpickled, "_objects") assert unpickled._objects == original_gl_objects @with_httmock(resp_get_user) def test_gitlab_token_auth(gl, callback=None): gl.auth() assert gl.user.username == username assert gl.user.id == user_id assert isinstance(gl.user, CurrentUser) def test_gitlab_from_config(default_config): config_path = default_config Gitlab.from_config("one", [config_path]) def test_gitlab_subclass_from_config(default_config): class MyGitlab(Gitlab): pass config_path = default_config gl = MyGitlab.from_config("one", [config_path]) assert isinstance(gl, MyGitlab) @pytest.mark.parametrize( "kwargs,expected_agent", [ ({}, USER_AGENT), ({"user_agent": "my-package/1.0.0"}, "my-package/1.0.0"), ], ) def test_gitlab_user_agent(kwargs, expected_agent): gl = Gitlab("http://localhost", **kwargs) assert gl.headers["User-Agent"] == expected_agent python-gitlab-2.10.1/tests/unit/test_gitlab_auth.py000066400000000000000000000050441416141341200223710ustar00rootroot00000000000000import pytest import requests from gitlab import Gitlab def test_invalid_auth_args(): with pytest.raises(ValueError): Gitlab( "http://localhost", api_version="4", private_token="private_token", oauth_token="bearer", ) with pytest.raises(ValueError): Gitlab( "http://localhost", api_version="4", oauth_token="bearer", http_username="foo", http_password="bar", ) with pytest.raises(ValueError): Gitlab( "http://localhost", api_version="4", private_token="private_token", http_password="bar", ) with pytest.raises(ValueError): Gitlab( "http://localhost", api_version="4", private_token="private_token", http_username="foo", ) def test_private_token_auth(): gl = Gitlab("http://localhost", private_token="private_token", api_version="4") assert gl.private_token == "private_token" assert gl.oauth_token is None assert gl.job_token is None assert gl._http_auth is None assert "Authorization" not in gl.headers assert gl.headers["PRIVATE-TOKEN"] == "private_token" assert "JOB-TOKEN" not in gl.headers def test_oauth_token_auth(): gl = Gitlab("http://localhost", oauth_token="oauth_token", api_version="4") assert gl.private_token is None assert gl.oauth_token == "oauth_token" assert gl.job_token is None assert gl._http_auth is None assert gl.headers["Authorization"] == "Bearer oauth_token" assert "PRIVATE-TOKEN" not in gl.headers assert "JOB-TOKEN" not in gl.headers def test_job_token_auth(): gl = Gitlab("http://localhost", job_token="CI_JOB_TOKEN", api_version="4") assert gl.private_token is None assert gl.oauth_token is None assert gl.job_token == "CI_JOB_TOKEN" assert gl._http_auth is None assert "Authorization" not in gl.headers assert "PRIVATE-TOKEN" not in gl.headers assert gl.headers["JOB-TOKEN"] == "CI_JOB_TOKEN" def test_http_auth(): gl = Gitlab( "http://localhost", private_token="private_token", http_username="foo", http_password="bar", api_version="4", ) assert gl.private_token == "private_token" assert gl.oauth_token is None assert gl.job_token is None assert isinstance(gl._http_auth, requests.auth.HTTPBasicAuth) assert gl.headers["PRIVATE-TOKEN"] == "private_token" assert "Authorization" not in gl.headers python-gitlab-2.10.1/tests/unit/test_gitlab_http_methods.py000066400000000000000000000175741416141341200241450ustar00rootroot00000000000000import pytest import requests from httmock import HTTMock, response, urlmatch from gitlab import GitlabHttpError, GitlabList, GitlabParsingError def test_build_url(gl): r = gl._build_url("http://localhost/api/v4") assert r == "http://localhost/api/v4" r = gl._build_url("https://localhost/api/v4") assert r == "https://localhost/api/v4" r = gl._build_url("/projects") assert r == "http://localhost/api/v4/projects" def test_http_request(gl): @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 = gl.http_request("get", "/projects") http_r.json() assert http_r.status_code == 200 def test_http_request_404(gl): @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): with pytest.raises(GitlabHttpError): gl.http_request("get", "/not_there") def test_get_request(gl): @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 = gl.http_get("/projects") assert isinstance(result, dict) assert result["name"] == "project1" def test_get_request_raw(gl): @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 = gl.http_get("/projects") assert result.content.decode("utf-8") == "content" def test_get_request_404(gl): @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): with pytest.raises(GitlabHttpError): gl.http_get("/not_there") def test_get_request_invalid_data(gl): @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): with pytest.raises(GitlabParsingError): gl.http_get("/projects") def test_list_request(gl): @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 = gl.http_list("/projects", as_list=True) assert isinstance(result, list) assert len(result) == 1 with HTTMock(resp_cont): result = gl.http_list("/projects", as_list=False) assert isinstance(result, GitlabList) assert len(result) == 1 with HTTMock(resp_cont): result = gl.http_list("/projects", all=True) assert isinstance(result, list) assert len(result) == 1 def test_list_request_404(gl): @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): with pytest.raises(GitlabHttpError): gl.http_list("/not_there") def test_list_request_invalid_data(gl): @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): with pytest.raises(GitlabParsingError): gl.http_list("/projects") def test_post_request(gl): @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 = gl.http_post("/projects") assert isinstance(result, dict) assert result["name"] == "project1" def test_post_request_404(gl): @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): with pytest.raises(GitlabHttpError): gl.http_post("/not_there") def test_post_request_invalid_data(gl): @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): with pytest.raises(GitlabParsingError): gl.http_post("/projects") def test_put_request(gl): @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 = gl.http_put("/projects") assert isinstance(result, dict) assert result["name"] == "project1" def test_put_request_404(gl): @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): with pytest.raises(GitlabHttpError): gl.http_put("/not_there") def test_put_request_invalid_data(gl): @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): with pytest.raises(GitlabParsingError): gl.http_put("/projects") def test_delete_request(gl): @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 = gl.http_delete("/projects") assert isinstance(result, requests.Response) assert result.json() is True def test_delete_request_404(gl): @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): with pytest.raises(GitlabHttpError): gl.http_delete("/not_there") python-gitlab-2.10.1/tests/unit/test_types.py000066400000000000000000000041011416141341200212430ustar00rootroot00000000000000# -*- 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 . from gitlab import types def test_gitlab_attribute_get(): o = types.GitlabAttribute("whatever") assert o.get() == "whatever" o.set_from_cli("whatever2") assert o.get() == "whatever2" assert o.get_for_api() == "whatever2" o = types.GitlabAttribute() assert o._value is None def test_list_attribute_input(): o = types.ListAttribute() o.set_from_cli("foo,bar,baz") assert o.get() == ["foo", "bar", "baz"] o.set_from_cli("foo") assert o.get() == ["foo"] def test_list_attribute_empty_input(): o = types.ListAttribute() o.set_from_cli("") assert o.get() == [] o.set_from_cli(" ") assert o.get() == [] def test_list_attribute_get_for_api_from_cli(): o = types.ListAttribute() o.set_from_cli("foo,bar,baz") assert o.get_for_api() == "foo,bar,baz" def test_list_attribute_get_for_api_from_list(): o = types.ListAttribute(["foo", "bar", "baz"]) assert o.get_for_api() == "foo,bar,baz" def test_list_attribute_get_for_api_from_int_list(): o = types.ListAttribute([1, 9, 7]) assert o.get_for_api() == "1,9,7" def test_list_attribute_does_not_split_string(): o = types.ListAttribute("foo") assert o.get_for_api() == "foo" def test_lowercase_string_attribute_get_for_api(): o = types.LowercaseStringAttribute("FOO") assert o.get_for_api() == "foo" python-gitlab-2.10.1/tests/unit/test_utils.py000066400000000000000000000025421416141341200212460ustar00rootroot00000000000000# -*- 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 . from gitlab import utils def test_clean_str_id(): src = "nothing_special" dest = "nothing_special" assert dest == utils.clean_str_id(src) src = "foo#bar/baz/" dest = "foo%23bar%2Fbaz%2F" assert dest == utils.clean_str_id(src) src = "foo%bar/baz/" dest = "foo%25bar%2Fbaz%2F" assert dest == utils.clean_str_id(src) def test_sanitized_url(): src = "http://localhost/foo/bar" dest = "http://localhost/foo/bar" assert dest == utils.sanitized_url(src) src = "http://localhost/foo.bar.baz" dest = "http://localhost/foo%2Ebar%2Ebaz" assert dest == utils.sanitized_url(src) python-gitlab-2.10.1/tox.ini000066400000000000000000000041751416141341200156730ustar00rootroot00000000000000[tox] minversion = 1.6 skipsdist = True envlist = py39,py38,py37,py36,pep8,black,twine-check,mypy,isort [testenv] passenv = GITLAB_IMAGE GITLAB_TAG setenv = VIRTUAL_ENV={envdir} whitelist_externals = true usedevelop = True install_command = pip install {opts} {packages} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/requirements-test.txt commands = pytest tests/unit {posargs} [testenv:pep8] basepython = python3 envdir={toxworkdir}/lint deps = -r{toxinidir}/requirements-lint.txt commands = flake8 {posargs} . [testenv:black] basepython = python3 envdir={toxworkdir}/lint deps = -r{toxinidir}/requirements-lint.txt commands = black {posargs} . [testenv:isort] basepython = python3 envdir={toxworkdir}/lint deps = -r{toxinidir}/requirements-lint.txt commands = isort {posargs} {toxinidir} [testenv:mypy] basepython = python3 envdir={toxworkdir}/lint deps = -r{toxinidir}/requirements-lint.txt commands = mypy {posargs} [testenv:twine-check] basepython = python3 deps = -r{toxinidir}/requirements.txt twine commands = python3 setup.py sdist bdist_wheel twine check dist/* [testenv:venv] commands = {posargs} [flake8] exclude = .git,.venv,.tox,dist,doc,*egg,build, max-line-length = 88 # We ignore the following because we use black to handle code-formatting # E203: Whitespace before ':' # E501: Line too long # W503: Line break occurred before a binary operator ignore = E203,E501,W503 per-file-ignores = gitlab/v4/objects/__init__.py:F401,F403 [testenv:docs] deps = -r{toxinidir}/requirements-docs.txt commands = python setup.py build_sphinx [testenv:cover] commands = pytest --cov --cov-report term --cov-report html \ --cov-report xml tests/unit {posargs} [coverage:run] omit = *tests* source = gitlab [coverage:report] exclude_lines = pragma: no cover if TYPE_CHECKING: if debug: [pytest] script_launch_mode = subprocess [testenv:cli_func_v4] deps = -r{toxinidir}/requirements-docker.txt commands = pytest --cov --cov-report xml tests/functional/cli {posargs} [testenv:py_func_v4] deps = -r{toxinidir}/requirements-docker.txt commands = pytest --cov --cov-report xml tests/functional/api {posargs}