├── VERSION ├── tests ├── __init__.py ├── mocks │ ├── message_ok_response.json │ ├── message_accepted_response.json │ ├── delete_context_response.json │ ├── get_envvar_response.json │ ├── follow_project_response.json │ ├── delete_context_envvar_response.json │ ├── get_pipeline_config_response.json │ ├── get_user_id_info_response.json │ ├── get_context_response.json │ ├── add_context_envvar_response.json │ ├── trigger_pipeline_response.json │ ├── get_project_branches_response.json │ ├── list_envvars_response.json │ ├── get_project_workflow_job_metrics_response.json │ ├── get_workflow_response.json │ ├── get_project_response.json │ ├── get_user_collaborations_response.json │ ├── get_pipeline_workflow_response.json │ ├── get_contexts_response.json │ ├── get_artifacts_response.json │ ├── get_context_envvars_response.json │ ├── get_project_workflow_metrics_response.json │ ├── get_workflow_jobs_response.json │ ├── get_checkout_key_response.json │ ├── create_checkout_key_response.json │ ├── get_schedule_response.json │ ├── list_checkout_keys_response.json │ ├── get_project_workflows_metrics_response.json │ ├── get_project_pipeline_response.json │ ├── get_schedules_response.json │ ├── get_latest_artifacts_response.json │ ├── get_pipeline_response.json │ ├── get_pipelines_response.json │ ├── get_test_metadata_response.json │ ├── get_job_details_response.json │ ├── get_flaky_tests_response.json │ ├── get_project_workflow_jobs_metrics_response.json │ ├── get_user_info_response.json │ ├── get_user_repos_response.json │ ├── get_project_pipelines_response.json │ ├── get_project_workflow_test_metrics_response.json │ ├── trigger_build_response.json │ ├── get_project_settings_response.json │ ├── retry_build_response.json │ ├── cancel_build_response.json │ ├── add_ssh_user_response.json │ ├── get_project_build_summary_response.json │ ├── get_projects_response.json │ ├── get_recent_builds_response.json │ └── get_build_info_response.json └── test_api.py ├── pycircleci ├── __init__.py ├── console.py └── api.py ├── pyproject.toml ├── requirements-dev.txt ├── .travis.yml ├── Makefile ├── .flake8 ├── .github └── workflows │ └── test.yml ├── LICENSE ├── CHANGELOG.md ├── setup.py ├── .gitignore └── README.md /VERSION: -------------------------------------------------------------------------------- 1 | 0.7.0 2 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pycircleci/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/mocks/message_ok_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": "ok" 3 | } 4 | -------------------------------------------------------------------------------- /tests/mocks/message_accepted_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": "Accepted." 3 | } 4 | -------------------------------------------------------------------------------- /tests/mocks/delete_context_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": "Context deleted." 3 | } 4 | -------------------------------------------------------------------------------- /tests/mocks/get_envvar_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foo", 3 | "value": "xxxxr" 4 | } 5 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | flake8~=6.0 2 | flake8-quotes~=3.0 3 | pytest 4 | requests 5 | requests-toolbelt 6 | -------------------------------------------------------------------------------- /tests/mocks/follow_project_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "mock+following": true, 3 | "first_build": null 4 | } 5 | -------------------------------------------------------------------------------- /tests/mocks/delete_context_envvar_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": "Environment variable deleted." 3 | } 4 | -------------------------------------------------------------------------------- /tests/mocks/get_pipeline_config_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "source configuration", 3 | "compiled": "compiled configuration" 4 | } 5 | -------------------------------------------------------------------------------- /tests/mocks/get_user_id_info_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "John", 3 | "login": "johndoe", 4 | "id": "deadbeef-dead-beef-dead-deaddeafbeef" 5 | } 6 | -------------------------------------------------------------------------------- /tests/mocks/get_context_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "deadbeef-dead-beef-dead-deaddeafbeef", 3 | "name": "testcontext", 4 | "created_at": "2015-09-21T17:29:21.042Z" 5 | } 6 | -------------------------------------------------------------------------------- /tests/mocks/add_context_envvar_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "context_id": "deadbeef-dead-beef-dead-deaddeafbeef", 3 | "created_at": "2021-11-06T00:25:40.393Z", 4 | "variable": "FOOBAR" 5 | } 6 | -------------------------------------------------------------------------------- /tests/mocks/trigger_pipeline_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "number": 12345, 3 | "state": "pending", 4 | "id": "deadbeef-dead-beef-dead-deaddeafbeef", 5 | "created_at": "2020-05-24T12:00:10.292Z" 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | dist: xenial 3 | python: 4 | - "3.6" 5 | - "3.7" 6 | install: 7 | - pip install flake8 8 | - pip install . 9 | script: 10 | - flake8 11 | - make test 12 | -------------------------------------------------------------------------------- /tests/mocks/get_project_branches_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "master", 4 | "test", 5 | "wip", 6 | "work" 7 | ], 8 | "org_id": null, 9 | "project_id": null 10 | } 11 | -------------------------------------------------------------------------------- /tests/mocks/list_envvars_response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "BAR", 4 | "value": "xxxxR" 5 | }, 6 | { 7 | "name": "BAZ", 8 | "value": "xxxxZ" 9 | }, 10 | { 11 | "name": "FOO", 12 | "value": "xxxxO" 13 | }, 14 | { 15 | "name": "foo", 16 | "value": "xxxxr" 17 | } 18 | ] 19 | -------------------------------------------------------------------------------- /tests/mocks/get_project_workflow_job_metrics_response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "deadbeef-dead-beef-dead-deaddeafbeef", 4 | "started_at": "2020-03-30T14:14:36.682Z", 5 | "stopped_at": "2020-03-30T14:16:47.436Z", 6 | "duration": 130, 7 | "status": "success", 8 | "credits_used": 22 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /tests/mocks/get_workflow_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "created_at": "2019-04-16T20:46:51Z", 3 | "id": "dummy-workflow-id", 4 | "name": "dummy-workflow", 5 | "pipeline_id": null, 6 | "pipeline_number": null, 7 | "project": { 8 | "id": "dummy-project" 9 | }, 10 | "status": "running", 11 | "stopped_at": null 12 | } 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean pack test 2 | 3 | clean: 4 | rm -rf build/ dist/ pycircleci.egg-info/ 5 | find ./ | grep -E "(__pycache__|\.pytest_cache|\.cache|\.pyc|\.pyo$$)" | xargs rm -rf 6 | 7 | console: 8 | python pycircleci/console.py 9 | 10 | pack: 11 | python setup.py sdist bdist_wheel 12 | 13 | test: 14 | pytest -v tests/ 15 | -------------------------------------------------------------------------------- /tests/mocks/get_project_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "slug": "gh/foo/bar", 3 | "name": "bar", 4 | "organization_name": "foo", 5 | "vcs_info": { 6 | "vcs_url": "https://github.com/foo/bar", 7 | "provider": "GitHub", 8 | "default_branch": "master" 9 | }, 10 | "last_job_finish_time": "2019-07-01T12:34:56.123Z" 11 | } 12 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | extend-ignore = 3 | # line length limit 4 | E501, 5 | # see https://github.com/PyCQA/pycodestyle/issues/373 6 | E203 7 | exclude = 8 | .git, 9 | .mypy_cache, 10 | .tox, 11 | .venv, 12 | __pycache__, 13 | _build, 14 | build, 15 | dist, 16 | temp, 17 | vendor 18 | inline-quotes = " 19 | -------------------------------------------------------------------------------- /tests/mocks/get_user_collaborations_response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "vcs_type": "github", 4 | "name": "johndoe", 5 | "avatar_url": "https://avatars.githubusercontent.com/u/123456?v=4" 6 | }, 7 | { 8 | "vcs_type": "github", 9 | "name": "org1", 10 | "avatar_url": "https://avatars.githubusercontent.com/u/9876543?v=4" 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /tests/mocks/get_pipeline_workflow_response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pipeline_id": "deadbeef-dead-beef-dead-deadbeef0001", 4 | "id": "deadbeef-dead-beef-dead-deadbeef0000", 5 | "name": "smoke-test", 6 | "project_slug": "gh/foo/bar", 7 | "status": "success", 8 | "started_by": "deadbeef-dead-beef-dead-deadbeef9999", 9 | "pipeline_number": 12345, 10 | "created_at": "2020-05-24T08:30:04Z", 11 | "stopped_at": "2020-05-24T08:31:20Z" 12 | } 13 | ] 14 | -------------------------------------------------------------------------------- /tests/mocks/get_contexts_response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "context1", 4 | "id": "deadbeef-dead-beef-dead-deaddeafbeef", 5 | "created_at": "2021-10-22T22:56:25.658Z" 6 | }, 7 | { 8 | "name": "context2", 9 | "id": "deadbeef-dead-beef-dead-deadbeef0002", 10 | "created_at": "2021-10-29T01:35:37.088Z" 11 | }, 12 | { 13 | "name": "foobar", 14 | "id": "deadbeef-dead-beef-dead-deadbeef0003", 15 | "created_at": "2021-10-19T03:40:58.198Z" 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /tests/mocks/get_artifacts_response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "path": "MOCK+raw-test-output/go-test-report.xml", 4 | "pretty_path": "raw-test-output/go-test-report.xml", 5 | "node_index": 0, 6 | "url": "https://24-88881093-gh.circle-artifacts.com/0/raw-test-output/go-test-report.xml" 7 | }, 8 | { 9 | "path": "raw-test-output/go-test.out", 10 | "pretty_path": "raw-test-output/go-test.out", 11 | "node_index": 0, 12 | "url": "https://24-88881093-gh.circle-artifacts.com/0/raw-test-output/go-test.out" 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /tests/mocks/get_context_envvars_response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "context_id": "deadbeef-dead-beef-dead-deaddeafbeef", 4 | "created_at": "2021-11-06T00:25:40.393Z", 5 | "variable": "foobar" 6 | }, 7 | { 8 | "context_id": "deadbeef-dead-beef-dead-deaddeafbeef", 9 | "created_at": "2021-11-06T00:42:03.148Z", 10 | "variable": "FOOBAR" 11 | }, 12 | { 13 | "context_id": "deadbeef-dead-beef-dead-deaddeafbeef", 14 | "created_at": "2021-11-06T00:42:11.094Z", 15 | "variable": "FOOBAR2" 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /tests/mocks/get_project_workflow_metrics_response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "deadbeef-dead-beef-dead-deadbeef0001", 4 | "status": "success", 5 | "duration": 178, 6 | "created_at": "2020-03-30T14:14:36.557Z", 7 | "stopped_at": "2020-03-30T14:17:35.011Z", 8 | "credits_used": 34 9 | }, 10 | { 11 | "id": "deadbeef-dead-beef-dead-deadbeef0002", 12 | "status": "success", 13 | "duration": 173, 14 | "created_at": "2020-03-28T19:42:26.010Z", 15 | "stopped_at": "2020-03-28T19:45:19.486Z", 16 | "credits_used": 28 17 | } 18 | ] 19 | -------------------------------------------------------------------------------- /tests/mocks/get_workflow_jobs_response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "1234", 4 | "name": "job1", 5 | "type": "build", 6 | "status": "canceled", 7 | "started_at": "2019-04-16T15:14:30Z", 8 | "dependencies": [], 9 | "job_number": 1, 10 | "stopped_at": "2019-04-16T15:31:42Z" 11 | }, 12 | { 13 | "id": "2345", 14 | "name": "job2", 15 | "type": "build", 16 | "status": "success", 17 | "started_at": "2019-04-16T15:14:31Z", 18 | "dependencies": [], 19 | "job_number": 2, 20 | "stopped_at": "2019-04-16T15:19:01Z" 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /tests/mocks/get_checkout_key_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC/fY7Kr3SiVzN6Tw7+5GjT7Qj5ldT92DcUAFJC/LJTRNcpTu1WtfuqEtdq3yoNm2PY4KgBViIecUuoeoKkfj9aQcuPFkol9RYeEbu80D21zke/aC4P4VFrCWaeLapEvFSSDF/lOSVGA7l+DEOoOtAcmIBOsPrkixyDmNf5orr9B0nZCOVlv6bq2sjIqJNEI1ER2sNY+ie1VZAA9kClMEd8rtGPKNC2T7couwiS3RR0jGob1VVuVuPw8B+JgWspg4RjpEKM9RLQpRb2mTDJBOmppogMNztXNav/WhRfdhDz8nlyI8liP1S19CDLFMaAHe2sH5hry8Qx8592yLt9Ght5 \n", 3 | "type": "deploy-key", 4 | "fingerprint": "94:19:ab:a9:f4:2b:21:1c:a5:87:dd:ee:3d:c2:90:4e", 5 | "login": null, 6 | "preferred": true, 7 | "time": "2017-10-25T08:19:19.213Z" 8 | } 9 | -------------------------------------------------------------------------------- /tests/mocks/create_checkout_key_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC/fY7Kr3SiVzN6Tw7+5GjT7Qj5ldT92DcUAFJC/LJTRNcpTu1WtfuqEtdq3yoNm2PY4KgBViIecUuoeoKkfj9aQcuPFkol9RYeEbu80D21zke/aC4P4VFrCWaeLapEvFSSDF/lOSVGA7l+DEOoOtAcmIBOsPrkixyDmNf5orr9B0nZCOVlv6bq2sjIqJNEI1ER2sNY+ie1VZAA9kClMEd8rtGPKNC2T7couwiS3RR0jGob1VVuVuPw8B+JgWspg4RjpEKM9RLQpRb2mTDJBOmppogMNztXNav/WhRfdhDz8nlyI8liP1S19CDLFMaAHe2sH5hry8Qx8592yLt9Ght5 \n", 3 | "type": "deploy-key", 4 | "fingerprint": "94:19:ab:a9:f4:2b:21:1c:a5:87:dd:ee:3d:c2:90:4e", 5 | "login": null, 6 | "preferred": true, 7 | "time": "2017-10-25T08:19:19.213Z" 8 | } 9 | -------------------------------------------------------------------------------- /tests/mocks/get_schedule_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "deadbeef-dead-beef-dead-deaddeafbeef", 3 | "name": "schedule1", 4 | "timetable": { 5 | "per-hour": 0, 6 | "hours-of-day": [ 7 | 0 8 | ], 9 | "days-of-week": [ 10 | "TUE" 11 | ] 12 | }, 13 | "description": "test schedule", 14 | "project-slug": "gh/foo/bar", 15 | "actor": { 16 | "id": "deadbeef-dead-beef-dead-deadbeef0000", 17 | "login": "johndoe", 18 | "name": "John" 19 | }, 20 | "created-at": "2019-08-24T14:15:22Z", 21 | "parameters": { 22 | "deploy_prod": true, 23 | "branch": "new_feature" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/mocks/list_checkout_keys_response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCEQcQIgpaJezfCyJM9BG4yu1ecsSoe0CAvnoO/U4B8PpwX6iB9CgqftQEkndanJSaPscMPw29L6KHS0RrMlXX8qHbkzB0C/qBy2hrhOKJLQpL6ez4LSjBJcC2FR6tjFMDHMBHlg5b6IzccNI6foyfHa7R557W+WCtBnSzLLk8AYyzF4A+G7ActXulI0UInwMxUBN8nr2VI4AhIRhzk2pIRYhEF8Mgs1IAmjbTmrWP1gPwOaqWBchch90h07HH7WwuRyHy076FGVMOB16fKK80SLaBeZ6KVygfkXF/7ZdkU4JbJUjlFs/JnJslXgR8SmMiqLRP1VxaNXbRTPaiwtEfj \n", 4 | "type": "deploy-key", 5 | "fingerprint": "9f:71:46:8b:a0:96:81:2d:53:b9:ad:0d:6d:e1:9c:51", 6 | "login": null, 7 | "preferred": true, 8 | "time": "2017-10-18T06:49:39.006Z" 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /tests/mocks/get_project_workflows_metrics_response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "build-and-test", 4 | "metrics": { 5 | "success_rate": 1.0, 6 | "total_runs": 3, 7 | "failed_runs": 0, 8 | "successful_runs": 3, 9 | "throughput": 0.15789473684210525, 10 | "mttr": 0, 11 | "duration_metrics": { 12 | "min": 78, 13 | "max": 178, 14 | "median": 173, 15 | "mean": 143, 16 | "p95": 177, 17 | "standard_deviation": 56.0 18 | }, 19 | "total_credits_used": 77 20 | }, 21 | "window_start": "2020-03-11T19:42:26.010Z", 22 | "window_end": "2020-03-30T14:17:35.011Z" 23 | } 24 | ] 25 | -------------------------------------------------------------------------------- /tests/mocks/get_project_pipeline_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "dummy-pipeline-id", 3 | "errors": [], 4 | "idempotency_key": "dummy-idempotency-key", 5 | "project_slug": "gh/foo/bar", 6 | "updated_at": "2019-07-01T12:34:56.123Z", 7 | "number": 1234, 8 | "state": "created", 9 | "created_at": "2019-07-01T12:34:56.123Z", 10 | "trigger": { 11 | "type": "webhook", 12 | "received_at": "2019-07-01T12:34:56.123Z" 13 | }, 14 | "vcs": { 15 | "provider_name": "GitHub", 16 | "origin_repository_url": "https://github.com/foo/bar", 17 | "target_repository_url": "https://github.com/foo/bar", 18 | "revision": "12345", 19 | "branch": "dummy" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/mocks/get_schedules_response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "deadbeef-dead-beef-dead-deaddeafbeef", 4 | "name": "schedule1", 5 | "timetable": { 6 | "per-hour": 0, 7 | "hours-of-day": [ 8 | 0 9 | ], 10 | "days-of-week": [ 11 | "TUE" 12 | ] 13 | }, 14 | "description": "schedule1", 15 | "project-slug": "gh/foo/bar", 16 | "actor": { 17 | "id": "deadbeef-dead-beef-dead-deadbeef0000", 18 | "login": "johndoe", 19 | "name": "John" 20 | }, 21 | "created-at": "2019-08-24T14:15:22Z", 22 | "parameters": { 23 | "deploy_prod": true, 24 | "branch": "new_feature" 25 | } 26 | } 27 | ] 28 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Test 3 | 4 | on: [push, pull_request] 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: ["3.8", "3.9", "3.10", "3.11"] 12 | name: Python ${{ matrix.python-version }} 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Setup python 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | architecture: x64 20 | - uses: BSFishy/pip-action@v1 21 | with: 22 | requirements: requirements-dev.txt 23 | - run: python -m flake8 24 | - run: python -m pytest -v 25 | -------------------------------------------------------------------------------- /tests/mocks/get_latest_artifacts_response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "path": "circleci-docs/index.html", 4 | "pretty_path": "circleci-docs/index.html", 5 | "node_index": 0, 6 | "url": "https://4149-48750547-gh.circle-artifacts.com/0/circleci-docs/index.html" 7 | }, 8 | { 9 | "path": "circleci-docs/sitemap.xml", 10 | "pretty_path": "circleci-docs/sitemap.xml", 11 | "node_index": 0, 12 | "url": "https://4149-48750547-gh.circle-artifacts.com/0/circleci-docs/sitemap.xml" 13 | }, 14 | { 15 | "path": "run-results/build-results.txt", 16 | "pretty_path": "run-results/build-results.txt", 17 | "node_index": 0, 18 | "url": "https://4149-48750547-gh.circle-artifacts.com/0/run-results/build-results.txt" 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /tests/mocks/get_pipeline_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "workflows": { 3 | "ids": [ 4 | "dummy-workflow-id" 5 | ], 6 | "total_count": 1 7 | }, 8 | "id": "dummy-pipeline-id", 9 | "errors": [], 10 | "idempotency_key": "dummy-idempotency-key", 11 | "project_slug": "gh/foo/bar", 12 | "updated_at": "2019-07-01T12:34:56.123Z", 13 | "number": 1234, 14 | "state": "created", 15 | "created_at": "2019-07-01T12:34:56.123Z", 16 | "trigger": { 17 | "type": "webhook", 18 | "received_at": "2019-07-01T12:34:56.123Z" 19 | }, 20 | "vcs": { 21 | "provider_name": "GitHub", 22 | "origin_repository_url": "https://github.com/foo/bar", 23 | "target_repository_url": "https://github.com/foo/bar", 24 | "revision": "12345", 25 | "branch": "dummy" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/mocks/get_pipelines_response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "workflows": { 4 | "ids": [ 5 | "dummy-workflow-id" 6 | ], 7 | "total_count": 1 8 | }, 9 | "id": "dummy-pipeline-id", 10 | "errors": [], 11 | "idempotency_key": "dummy-idempotency-key", 12 | "project_slug": "gh/foo/bar", 13 | "updated_at": "2019-07-01T12:34:56.123Z", 14 | "number": 1234, 15 | "state": "created", 16 | "created_at": "2019-07-01T12:34:56.123Z", 17 | "trigger": { 18 | "type": "webhook", 19 | "received_at": "2019-07-01T12:34:56.123Z" 20 | }, 21 | "vcs": { 22 | "provider_name": "GitHub", 23 | "origin_repository_url": "https://github.com/foo/bar", 24 | "target_repository_url": "https://github.com/foo/bar", 25 | "revision": "12345", 26 | "branch": "dummy" 27 | } 28 | } 29 | ] 30 | -------------------------------------------------------------------------------- /pycircleci/console.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import code 3 | import sys 4 | from pprint import pprint as pp # noqa: F401 5 | 6 | from pycircleci.api import Api 7 | 8 | 9 | def console(): 10 | """Start a CircleCI client interactive console""" 11 | _pyver = f"Python {sys.version}" 12 | _msg = 'Type "man()" to show the Help screen.' 13 | _title = "(CircleCI client InteractiveConsole)" 14 | _banner = f"{_pyver}\n{_msg}\n{_title}" 15 | 16 | def man(): 17 | _txt = """ 18 | === CircleCI client Console Help === 19 | 20 | The following VARIABLES are directly accessible in a CircleCI client session: 21 | 22 | c, client # an initialized instance of CircleCI Api class 23 | """ 24 | print(_txt) 25 | 26 | c = client = Api() 27 | code.interact(banner=_banner, local=dict(globals(), **locals())) 28 | 29 | 30 | if __name__ == "__main__": 31 | console() 32 | -------------------------------------------------------------------------------- /tests/mocks/get_test_metadata_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "tests": [ 3 | { 4 | "classname": "", 5 | "file": null, 6 | "name": "client › modules › App › AppReducer › action for TOGGLE_ADD_POST is working", 7 | "result": "success", 8 | "run_time": 0, 9 | "message": null, 10 | "source": "unknown", 11 | "source_type": "unknown" 12 | }, 13 | { 14 | "classname": "", 15 | "file": null, 16 | "name": "client › modules › App › AppActions › should return the correct type for toggleAddPost", 17 | "result": "success", 18 | "run_time": 0, 19 | "message": null, 20 | "source": "unknown", 21 | "source_type": "unknown" 22 | }, 23 | { 24 | "classname": "", 25 | "file": null, 26 | "name": "client › modules › App › AppReducer › getShowAddPost selector", 27 | "result": "success", 28 | "run_time": 0, 29 | "message": null, 30 | "source": "unknown", 31 | "source_type": "unknown" 32 | } 33 | ], 34 | "exceptions": [] 35 | } 36 | -------------------------------------------------------------------------------- /tests/mocks/get_job_details_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "web_url": "https://circleci.com/gh/foo/bar/12345", 3 | "project": { 4 | "external_url": "https://github.com/foo/bar", 5 | "slug": "gh/foo/bar", 6 | "name": "bar" 7 | }, 8 | "parallel_runs": [ 9 | { 10 | "index": 0, 11 | "status": "success" 12 | } 13 | ], 14 | "started_at": "2021-11-05T21:40:24.565Z", 15 | "latest_workflow": { 16 | "name": "foobar-test", 17 | "id": "deadbeef-dead-beef-dead-deadbeef0001" 18 | }, 19 | "name": "foobar_test", 20 | "executor": { 21 | "resource_class": "medium", 22 | "type": "docker" 23 | }, 24 | "parallelism": 1, 25 | "status": "success", 26 | "number": 12345, 27 | "pipeline": { 28 | "id": "deadbeef-dead-beef-dead-deadbeef0002" 29 | }, 30 | "duration": 65359, 31 | "created_at": "2021-11-05T21:40:21.312Z", 32 | "messages": [], 33 | "contexts": [], 34 | "organization": { 35 | "name": "foo" 36 | }, 37 | "queued_at": "2021-11-05T21:40:21.385Z", 38 | "stopped_at": "2021-11-05T21:41:29.924Z" 39 | } 40 | -------------------------------------------------------------------------------- /tests/mocks/get_flaky_tests_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "flaky_tests": [ 3 | { 4 | "workflow_created_at": "2023-04-24T16:48:19Z", 5 | "classname": "yea", 6 | "job_number": 547148, 7 | "times_flaked": 10, 8 | "source": "", 9 | "pipeline_number": 21702, 10 | "file": "", 11 | "workflow_name": "main", 12 | "job_name": "func-win-base-py39", 13 | "workflow_id": "0cc5a532-ca97-48e4-9fab-da01bffaae11", 14 | "time_wasted": 0, 15 | "test_name": "0.debug.8" 16 | }, 17 | { 18 | "workflow_created_at": "2023-04-26T21:55:20Z", 19 | "classname": "tests.pytest_tests.unit_tests.test_lib.test_filesystem", 20 | "job_number": 554986, 21 | "times_flaked": 5, 22 | "source": "", 23 | "pipeline_number": 22837, 24 | "file": "", 25 | "workflow_name": "main", 26 | "job_name": "unit-win-py39", 27 | "workflow_id": "51fa7e3a-0e7f-425b-b970-c9fe346e9876", 28 | "time_wasted": 0, 29 | "test_name": "test_safe_copy_with_file_changes" 30 | } 31 | ], 32 | "total_flaky_tests": 2 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Adrian Kazaku 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/mocks/get_project_workflow_jobs_metrics_response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "test", 4 | "metrics": { 5 | "success_rate": 1.0, 6 | "total_runs": 3, 7 | "failed_runs": 0, 8 | "successful_runs": 3, 9 | "throughput": 0.15789473684210525, 10 | "duration_metrics": { 11 | "min": 16, 12 | "max": 36, 13 | "median": 31, 14 | "mean": 28, 15 | "p95": 35, 16 | "standard_deviation": 10.0 17 | }, 18 | "total_credits_used": 10 19 | }, 20 | "window_start": "2020-03-11T19:42:26.191Z", 21 | "window_end": "2020-03-30T14:14:53.175Z" 22 | }, 23 | { 24 | "name": "build", 25 | "metrics": { 26 | "success_rate": 1.0, 27 | "total_runs": 1, 28 | "failed_runs": 0, 29 | "successful_runs": 1, 30 | "throughput": 1.0, 31 | "duration_metrics": { 32 | "min": 130, 33 | "max": 130, 34 | "median": 130, 35 | "mean": 130, 36 | "p95": 130, 37 | "standard_deviation": 0.0 38 | }, 39 | "total_credits_used": 22 40 | }, 41 | "window_start": "2020-03-30T14:14:36.682Z", 42 | "window_end": "2020-03-30T14:16:47.436Z" 43 | } 44 | ] 45 | -------------------------------------------------------------------------------- /tests/mocks/get_user_info_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "enrolled_betas": [], 3 | "in_beta_program": false, 4 | "selected_email": "mock+ccie-tester@circleci.com", 5 | "avatar_url": "https://ghe-dev.circleci.com/avatars/u/20?", 6 | "trial_end": "2017-11-05T04:41:09.780Z", 7 | "admin": null, 8 | "basic_email_prefs": "smart", 9 | "sign_in_count": 1, 10 | "github_oauth_scopes": [ 11 | "user:email", 12 | "repo" 13 | ], 14 | "analytics_id": "deadbeef-dead-beef-dead-deaddeafbeef", 15 | "name": null, 16 | "gravatar_id": null, 17 | "first_vcs_authorized_client_id": -1, 18 | "days_left_in_trial": 13, 19 | "parallelism": 1, 20 | "student": false, 21 | "bitbucket_authorized": false, 22 | "github_id": 20, 23 | "bitbucket": null, 24 | "dev_admin": false, 25 | "all_emails": [ 26 | "+ccie-tester@circleci.com", 27 | "+ccie-tester@circleci.com" 28 | ], 29 | "created_at": "2017-10-22T04:41:09.780Z", 30 | "plan": null, 31 | "heroku_api_key": null, 32 | "identities": { 33 | "github": { 34 | "avatar_url": "https://ghe-dev.circleci.com/avatars/u/20?", 35 | "external_id": 20, 36 | "id": 20, 37 | "name": null, 38 | "user?": true, 39 | "domain": "ghe-dev.circleci.com", 40 | "type": "github", 41 | "authorized?": true, 42 | "login": "ccie-tester" 43 | } 44 | }, 45 | "projects": {}, 46 | "login": "ccie-tester", 47 | "organization_prefs": {}, 48 | "containers": 1, 49 | "pusher_id": "1a5200e7733c334e751c85cc1ae3c1aee7387143", 50 | "num_projects_followed": 0 51 | } 52 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### [0.7.0] - 2023-04-29 2 | * Add endpoint to get flaky tests (API v2) 3 | * Add build system info to comply with PEP-518 4 | 5 | ### [0.6.1] - 2023-02-04 6 | * Refactor string formatting to use f-strings 7 | 8 | ### [0.6.0] - 2023-01-30 9 | * Add endpoint to get user repos 10 | 11 | ### [0.5.2] - 2022-05-26 12 | * Add support for Circle-Token header based auth 13 | 14 | ### [0.5.1] - 2022-01-30 15 | * Update deprecated Retry option 16 | 17 | ### [0.5.0] - 2021-11-10 18 | * Add more insights endpoints (API v2) 19 | * Add more pipeline endpoints (API v2) 20 | * Add more user endpoints (API v2) 21 | * Add more workflow endpoints (API v2) 22 | * Add schedule endpoints (API v2) 23 | 24 | ### [0.4.1] - 2021-11-07 25 | * Fix get_contexts() 26 | * Add endpoints for context environment variables (API v2) 27 | 28 | ### [0.4.0] - 2021-11-06 29 | * Add context endpoints (API v2) 30 | * Add job details endpoint (API v2) 31 | * Add support for pagination: the results from endpoints that support pagination will come as a list rather than a list under `response["items"]` 32 | 33 | ### [0.3.2] - 2021-03-30 34 | * Add response code 429 to the list of HTTP status codes to force a retry on 35 | 36 | ### [0.3.1] - 2020-08-01 37 | * Add job approval endpoint (API v2) 38 | 39 | ### [0.3.0] - 2020-05-24 40 | * Add endpoints to get project pipelines (API v2) 41 | * Add endpoints to get project insights (API v2) 42 | 43 | ### [0.2.0] - 2020-02-20 44 | * Add endpoints to get/update project advanced settings 45 | 46 | ### [0.1.0] - 2020-02-18 47 | * Initial public version 48 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import find_packages, setup 4 | 5 | 6 | def version(): 7 | path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "VERSION") 8 | with open(path) as f: 9 | ver = f.read().strip() 10 | return ver 11 | 12 | 13 | with open("README.md", "r") as f: 14 | long_description = f.read() 15 | 16 | 17 | setup( 18 | name="pycircleci", 19 | version=version(), 20 | description="Python client for CircleCI API", 21 | long_description=long_description, 22 | long_description_content_type="text/markdown", 23 | url="https://github.com/alpinweis/pycircleci", 24 | author="Adrian Kazaku", 25 | author_email="alpinweis@gmail.com", 26 | license="MIT", 27 | classifiers=[ 28 | "Development Status :: 5 - Production/Stable", 29 | "Intended Audience :: Developers", 30 | "Intended Audience :: System Administrators", 31 | "License :: OSI Approved :: MIT License", 32 | "Operating System :: OS Independent", 33 | "Topic :: Software Development :: Build Tools", 34 | "Topic :: Software Development :: Libraries :: Python Modules", 35 | "Topic :: Internet", 36 | "Programming Language :: Python :: 3", 37 | "Programming Language :: Python :: 3.8", 38 | "Programming Language :: Python :: 3.9", 39 | "Programming Language :: Python :: 3.10", 40 | "Programming Language :: Python :: 3.11", 41 | "Programming Language :: Python :: 3 :: Only", 42 | ], 43 | keywords="circleci ci cd api", 44 | packages=find_packages(), 45 | install_requires=["requests", "requests-toolbelt"], 46 | python_requires=">=3.6", 47 | zip_safe=False, 48 | ) 49 | -------------------------------------------------------------------------------- /tests/mocks/get_user_repos_response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pushed_at": "2023-01-26T06:08:43Z", 4 | "owner": { 5 | "login": "foobar", 6 | "external_id": 12345699, 7 | "avatar_url": "https://avatars.githubusercontent.com/u/12345699?v=4", 8 | "name": "foobar" 9 | }, 10 | "has_followers": false, 11 | "username": "foobar", 12 | "admin": true, 13 | "name": "repo1", 14 | "vcs_type": "github", 15 | "created_at": "2019-10-02T14:24:30Z", 16 | "fork": false, 17 | "language": "TypeScript", 18 | "vcs_url": "https://github.com/foobar/repo1", 19 | "following": false 20 | }, 21 | { 22 | "pushed_at": "2023-01-26T04:35:10Z", 23 | "owner": { 24 | "login": "foobar", 25 | "external_id": 12345699, 26 | "avatar_url": "https://avatars.githubusercontent.com/u/12345699?v=4", 27 | "name": "foobar" 28 | }, 29 | "has_followers": true, 30 | "username": "foobar", 31 | "admin": true, 32 | "name": "repo2", 33 | "vcs_type": "github", 34 | "created_at": "2018-10-15T22:20:17Z", 35 | "fork": false, 36 | "language": "JavaScript", 37 | "vcs_url": "https://github.com/foobar/repo2", 38 | "following": true 39 | }, 40 | { 41 | "pushed_at": "2018-10-09T17:32:27Z", 42 | "owner": { 43 | "login": "otherorg", 44 | "external_id": 10888892, 45 | "avatar_url": "https://avatars.githubusercontent.com/u/10888892?v=4", 46 | "name": "otherorg" 47 | }, 48 | "has_followers": false, 49 | "username": "otherorg", 50 | "admin": false, 51 | "name": "repo3", 52 | "vcs_type": "github", 53 | "created_at": "2015-02-10T21:47:32Z", 54 | "fork": false, 55 | "language": "Ruby", 56 | "vcs_url": "https://github.com/otherorg/repo3", 57 | "following": false 58 | } 59 | ] 60 | -------------------------------------------------------------------------------- /tests/mocks/get_project_pipelines_response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "deadbeef-dead-beef-dead-deadbeef0001", 4 | "errors": [], 5 | "project_slug": "gh/foo/bar", 6 | "updated_at": "2020-05-06T15:36:05.279Z", 7 | "number": 12345, 8 | "state": "created", 9 | "created_at": "2020-05-06T15:36:05.279Z", 10 | "trigger": { 11 | "received_at": "2020-05-06T15:36:05.239Z", 12 | "type": "webhook", 13 | "actor": { 14 | "login": "user", 15 | "avatar_url": "https://avatars1.githubusercontent.com/u/1234?v=4" 16 | } 17 | }, 18 | "vcs": { 19 | "origin_repository_url": "https://github.com/foo/bar", 20 | "target_repository_url": "https://github.com/foo/bar", 21 | "revision": "3a82a4bf50efded96b06e726fc52e8522e409d7c", 22 | "provider_name": "GitHub", 23 | "commit": { 24 | "body": "", 25 | "subject": "test commit 1" 26 | }, 27 | "branch": "develop" 28 | } 29 | }, 30 | { 31 | "id": "deadbeef-dead-beef-dead-deadbeef0002", 32 | "errors": [], 33 | "project_slug": "gh/foo/bar", 34 | "updated_at": "2020-05-06T16:36:05.123Z", 35 | "number": 12346, 36 | "state": "created", 37 | "created_at": "2020-05-06T15:36:05.279Z", 38 | "trigger": { 39 | "received_at": "2020-05-06T15:36:05.239Z", 40 | "type": "webhook", 41 | "actor": { 42 | "login": "user", 43 | "avatar_url": "https://avatars1.githubusercontent.com/u/1234?v=4" 44 | } 45 | }, 46 | "vcs": { 47 | "origin_repository_url": "https://github.com/foo/bar", 48 | "target_repository_url": "https://github.com/foo/bar", 49 | "revision": "4b93a4bf50efded96b06e726fc52e8522e409d7c", 50 | "provider_name": "GitHub", 51 | "commit": { 52 | "body": "", 53 | "subject": "test commit 2" 54 | }, 55 | "branch": "develop" 56 | } 57 | } 58 | ] 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .nox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | *.py,cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | db.sqlite3-journal 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # pipenv 86 | #Pipfile.lock 87 | 88 | # PEP 582 89 | __pypackages__/ 90 | 91 | # Celery stuff 92 | celerybeat-schedule 93 | celerybeat.pid 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # Environments 99 | .env 100 | .venv 101 | env/ 102 | venv/ 103 | ENV/ 104 | env.bak/ 105 | venv.bak/ 106 | 107 | # Spyder project settings 108 | .spyderproject 109 | .spyproject 110 | 111 | # Rope project settings 112 | .ropeproject 113 | 114 | # mkdocs documentation 115 | /site 116 | 117 | # mypy 118 | .mypy_cache/ 119 | .dmypy.json 120 | dmypy.json 121 | 122 | # Pyre type checker 123 | .pyre/ 124 | 125 | # pytype static type analyzer 126 | .pytype/ 127 | -------------------------------------------------------------------------------- /tests/mocks/get_project_workflow_test_metrics_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "average_test_count": 2, 3 | "most_failed_tests": [ 4 | { 5 | "failed_runs": 1, 6 | "flaky": false, 7 | "job_name": "job1", 8 | "p95_duration": 20.864, 9 | "test_name": "test1", 10 | "total_runs": 2 11 | }, 12 | { 13 | "failed_runs": 1, 14 | "flaky": false, 15 | "job_name": "job2", 16 | "p95_duration": 0.001, 17 | "test_name": "test2", 18 | "total_runs": 1 19 | } 20 | ], 21 | "most_failed_tests_extra": 2, 22 | "slowest_tests": [ 23 | { 24 | "failed_runs": 0, 25 | "flaky": false, 26 | "job_name": "job3", 27 | "p95_duration": 141.386, 28 | "test_name": "test3", 29 | "total_runs": 1 30 | }, 31 | { 32 | "failed_runs": 0, 33 | "flaky": false, 34 | "job_name": "job4", 35 | "p95_duration": 141.144, 36 | "test_name": "test4", 37 | "total_runs": 1 38 | } 39 | ], 40 | "slowest_tests_extra": 0, 41 | "test_runs": [ 42 | { 43 | "pipeline_number": 1234, 44 | "success_rate": 0, 45 | "test_counts": { 46 | "error": 0, 47 | "failure": 0, 48 | "skipped": 2, 49 | "success": 0, 50 | "total": 77 51 | }, 52 | "workflow_id": "deadbeef-dead-beef-dead-deadbeef0001" 53 | }, 54 | { 55 | "pipeline_number": 1235, 56 | "success_rate": 1, 57 | "test_counts": { 58 | "error": 0, 59 | "failure": 0, 60 | "skipped": 2, 61 | "success": 73, 62 | "total": 77 63 | }, 64 | "workflow_id": "deadbeef-dead-beef-dead-deadbeef0002" 65 | }, 66 | { 67 | "pipeline_number": 1236, 68 | "success_rate": 0, 69 | "test_counts": { 70 | "error": 0, 71 | "failure": 0, 72 | "skipped": 2, 73 | "success": 0, 74 | "total": 77 75 | }, 76 | "workflow_id": "deadbeef-dead-beef-dead-deadbeef0003" 77 | } 78 | ], 79 | "total_test_runs": 3 80 | } 81 | -------------------------------------------------------------------------------- /tests/mocks/trigger_build_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "compare": null, 3 | "previous_successful_build": null, 4 | "build_parameters": {}, 5 | "oss": true, 6 | "committer_date": null, 7 | "body": null, 8 | "usage_queued_at": "2017-10-23T04:37:14.638Z", 9 | "fail_reason": null, 10 | "retry_of": null, 11 | "reponame": "MOCK+testing", 12 | "ssh_users": [], 13 | "build_url": "https://ccie-preview.sphereci.com/gh/ccie-tester/testing/5", 14 | "parallel": 1, 15 | "failed": null, 16 | "branch": "master", 17 | "username": "ccie-tester", 18 | "author_date": null, 19 | "why": "api", 20 | "user": { 21 | "is_user": true, 22 | "login": "ccie-tester", 23 | "avatar_url": "https://ghe-dev.circleci.com/avatars/u/20?", 24 | "name": null, 25 | "vcs_type": "github", 26 | "id": 20 27 | }, 28 | "vcs_revision": "852a5da11d9a21cb9d272f638f51a64cd2bf2998", 29 | "vcs_tag": null, 30 | "build_num": 5, 31 | "infrastructure_fail": false, 32 | "committer_email": null, 33 | "previous": { 34 | "build_num": 4, 35 | "status": "infrastructure_fail", 36 | "build_time_millis": 0 37 | }, 38 | "status": "not_running", 39 | "committer_name": null, 40 | "retries": null, 41 | "subject": null, 42 | "vcs_type": "github", 43 | "timedout": false, 44 | "dont_build": null, 45 | "lifecycle": "not_running", 46 | "no_dependency_cache": false, 47 | "stop_time": null, 48 | "ssh_disabled": false, 49 | "build_time_millis": null, 50 | "picard": null, 51 | "circle_yml": { 52 | "string": "general:\n artifacts:\n - \"artifact.txt\"\n\ndependencies:\n pre:\n # fixed_bug https://circleci.atlassian.net/browse/CIRCLE-7174\n sudo service postgresql restart\n\ntest:\n override:\n - mkdir -p $CIRCLE_TEST_REPORTS/junit\n - cp test-results.xml $CIRCLE_TEST_REPORTS/junit/test-results.xml\n\n" 53 | }, 54 | "messages": [], 55 | "is_first_green_build": false, 56 | "job_name": null, 57 | "start_time": null, 58 | "canceler": null, 59 | "platform": "1.0", 60 | "outcome": null, 61 | "vcs_url": "https://ghe-dev.circleci.com/ccie-tester/testing", 62 | "author_name": null, 63 | "node": null, 64 | "canceled": false, 65 | "author_email": null 66 | } 67 | -------------------------------------------------------------------------------- /tests/mocks/get_project_settings_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "irc_server": null, 3 | "slack_integration_channel": null, 4 | "scopes": [ 5 | "write-settings", 6 | "view-builds", 7 | "read-settings", 8 | "trigger-builds", 9 | "all", 10 | "status", 11 | "none" 12 | ], 13 | "irc_keyword": null, 14 | "slack_integration_team_id": null, 15 | "vcs-type": "github", 16 | "aws": { "keypair": null }, 17 | "slack_webhook_url": null, 18 | "flowdock_api_token": null, 19 | "parallel": 1, 20 | "slack_integration_team": null, 21 | "username": "user", 22 | "campfire_room": null, 23 | "extra": "", 24 | "branches": { 25 | "master": { 26 | "running_builds": [], 27 | "recent_builds": [], 28 | "latest_workflows": {}, 29 | "pusher_logins": [], 30 | "is_using_workflows": true 31 | } 32 | }, 33 | "jira": null, 34 | "slack_integration_notify_prefs": null, 35 | "slack_integration_webhook_url": null, 36 | "slack_subdomain": null, 37 | "following": true, 38 | "setup": "", 39 | "campfire_subdomain": null, 40 | "slack_notify_prefs": null, 41 | "irc_password": null, 42 | "vcs_url": "https://github.com/user/circleci-sandbox", 43 | "default_branch": "master", 44 | "hipchat_api_token": null, 45 | "irc_username": null, 46 | "language": null, 47 | "slack_channel_override": null, 48 | "hipchat_notify": null, 49 | "slack_api_token": null, 50 | "has_usable_key": true, 51 | "irc_notify_prefs": null, 52 | "campfire_token": null, 53 | "slack_channel": null, 54 | "feature_flags": { 55 | "trusty-beta": false, 56 | "builds-service": true, 57 | "osx": false, 58 | "set-github-status": false, 59 | "build-prs-only": false, 60 | "forks-receive-secret-env-vars": false, 61 | "fleet": null, 62 | "build-fork-prs": false, 63 | "autocancel-builds": true 64 | }, 65 | "campfire_notify_prefs": null, 66 | "hipchat_room": null, 67 | "heroku_deploy_user": null, 68 | "slack_integration_channel_id": null, 69 | "irc_channel": null, 70 | "oss": false, 71 | "reponame": "circleci-sandbox", 72 | "hipchat_notify_prefs": null, 73 | "compile": "", 74 | "dependencies": "", 75 | "slack_integration_access_token": null, 76 | "test": "", 77 | "ssh_keys": [] 78 | } 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pycircleci 2 | 3 | [![PyPI version](https://img.shields.io/pypi/v/pycircleci?color=blue)](https://python.org/pypi/pycircleci) 4 | [![Build Status](https://github.com/alpinweis/pycircleci/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/alpinweis/pycircleci/actions/workflows/test.yml?query=branch%3Amaster) 5 | 6 | Python client for [CircleCI API](https://circleci.com/docs/2.0/api-intro/). 7 | 8 | Based on the discontinued [circleci.py](https://github.com/levlaz/circleci.py) project. 9 | 10 | ## Features 11 | 12 | - Supports [API v1.1](https://circleci.com/docs/api/#api-overview) and [API v2](https://circleci.com/docs/api/v2/) 13 | - Supports both `circleci.com` and self-hosted [Enterprise CircleCI](https://circleci.com/enterprise/) 14 | 15 | ## Installation 16 | 17 | $ pip install pycircleci 18 | 19 | ## Usage 20 | 21 | Create a personal [API token](https://circleci.com/docs/2.0/managing-api-tokens/#creating-a-personal-api-token). 22 | 23 | Set up the expected env vars: 24 | 25 | CIRCLE_TOKEN # CircleCI API access token 26 | CIRCLE_API_URL # CircleCI API base url. Defaults to https://circleci.com/api 27 | 28 | ```python 29 | from pycircleci.api import Api, CIRCLE_TOKEN, CIRCLE_API_URL 30 | 31 | circle_client = Api(token=CIRCLE_TOKEN, url=CIRCLE_API_URL) 32 | 33 | # get current user info 34 | circle_client.get_user_info() 35 | 36 | # get list of projects 37 | results = circle_client.get_projects() 38 | 39 | # pretty print results as json 40 | circle_client.ppj(results) 41 | 42 | # pretty print the details of the last request/response 43 | circle_client.ppr() 44 | ``` 45 | 46 | ### Interactive development console 47 | 48 | make console 49 | 50 | This starts a pre-configured python interactive console which gives you access to a 51 | `client` object - an instance of the `Api` class to play around. From the console 52 | type `man()` to see the help screen. 53 | 54 | ### Contributing 55 | 56 | 1. Fork it 57 | 1. Install dev dependencies (`pip install -r requirements-dev.txt`) 58 | 1. Create your feature branch (`git checkout -b my-new-feature`) 59 | 1. Make sure `flake8` and the `pytest` test suite successfully run locally 60 | 1. Commit your changes (`git commit -am 'Add some feature'`) 61 | 1. Push to the branch (`git push origin my-new-feature`) 62 | 1. Create new Pull Request 63 | -------------------------------------------------------------------------------- /tests/mocks/retry_build_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "compare": null, 3 | "previous_successful_build": null, 4 | "build_parameters": null, 5 | "oss": true, 6 | "committer_date": "2017-10-23T00:52:25Z", 7 | "body": "", 8 | "usage_queued_at": "2017-10-23T04:58:24.849Z", 9 | "fail_reason": null, 10 | "retry_of": 1, 11 | "reponame": "MOCK+testing", 12 | "ssh_users": [], 13 | "build_url": "https://ccie-preview.sphereci.com/gh/ccie-tester/testing/9", 14 | "parallel": 1, 15 | "failed": null, 16 | "branch": "master", 17 | "username": "ccie-tester", 18 | "author_date": "2017-10-23T00:52:25Z", 19 | "why": "retry", 20 | "user": { 21 | "is_user": true, 22 | "login": "ccie-tester", 23 | "avatar_url": "https://ghe-dev.circleci.com/avatars/u/20?", 24 | "name": null, 25 | "vcs_type": "github", 26 | "id": 20 27 | }, 28 | "vcs_revision": "852a5da11d9a21cb9d272f638f51a64cd2bf2998", 29 | "vcs_tag": null, 30 | "build_num": 9, 31 | "infrastructure_fail": false, 32 | "committer_email": "nathan+ccie-tester@circleci.com", 33 | "previous": { 34 | "build_num": 1, 35 | "status": "retried", 36 | "build_time_millis": 0 37 | }, 38 | "status": "scheduled", 39 | "committer_name": "ccie-tester", 40 | "retries": null, 41 | "subject": "Update circle.yml", 42 | "vcs_type": "github", 43 | "timedout": false, 44 | "dont_build": null, 45 | "lifecycle": "scheduled", 46 | "no_dependency_cache": false, 47 | "stop_time": null, 48 | "ssh_disabled": false, 49 | "build_time_millis": null, 50 | "picard": null, 51 | "circle_yml": { 52 | "string": "general:\n artifacts:\n - \"artifact.txt\"\n\ndependencies:\n pre:\n # fixed_bug https://circleci.atlassian.net/browse/CIRCLE-7174\n sudo service postgresql restart\n\ntest:\n override:\n - mkdir -p $CIRCLE_TEST_REPORTS/junit\n - cp test-results.xml $CIRCLE_TEST_REPORTS/junit/test-results.xml\n\n" 53 | }, 54 | "messages": [], 55 | "is_first_green_build": false, 56 | "job_name": null, 57 | "start_time": null, 58 | "canceler": null, 59 | "all_commit_details": [ 60 | { 61 | "committer_date": "2017-10-23T00:52:25Z", 62 | "body": "", 63 | "author_date": "2017-10-23T00:52:25Z", 64 | "committer_email": "nathan+ccie-tester@circleci.com", 65 | "commit": "852a5da11d9a21cb9d272f638f51a64cd2bf2998", 66 | "committer_login": "ccie-tester", 67 | "committer_name": "ccie-tester", 68 | "subject": "Update circle.yml", 69 | "commit_url": "https://ghe-dev.circleci.com/ccie-tester/testing/commit/852a5da11d9a21cb9d272f638f51a64cd2bf2998", 70 | "author_login": "ccie-tester", 71 | "author_name": "ccie-tester", 72 | "author_email": "nathan+ccie-tester@circleci.com" 73 | } 74 | ], 75 | "platform": "1.0", 76 | "outcome": null, 77 | "vcs_url": "https://ghe-dev.circleci.com/ccie-tester/testing", 78 | "author_name": "ccie-tester", 79 | "node": null, 80 | "canceled": false, 81 | "author_email": "nathan+ccie-tester@circleci.com" 82 | } 83 | -------------------------------------------------------------------------------- /tests/mocks/cancel_build_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "compare": null, 3 | "previous_successful_build": null, 4 | "build_parameters": null, 5 | "oss": true, 6 | "committer_date": "2017-10-23T00:52:25Z", 7 | "body": "", 8 | "fail_reason": null, 9 | "retry_of": 10, 10 | "reponame": "MOCK+testing", 11 | "ssh_users": [], 12 | "build_url": "https://ccie-preview.sphereci.com/gh/ccie-tester/testing/11", 13 | "parallel": 1, 14 | "failed": null, 15 | "branch": "master", 16 | "username": "ccie-tester", 17 | "author_date": "2017-10-23T00:52:25Z", 18 | "why": "auto-retry", 19 | "user": { 20 | "is_user": true, 21 | "login": "ccie-tester", 22 | "avatar_url": "https://ghe-dev.circleci.com/avatars/u/20?", 23 | "name": null, 24 | "vcs_type": "github", 25 | "id": 20 26 | }, 27 | "vcs_revision": "852a5da11d9a21cb9d272f638f51a64cd2bf2998", 28 | "vcs_tag": null, 29 | "build_num": 11, 30 | "infrastructure_fail": false, 31 | "committer_email": "nathan+ccie-tester@circleci.com", 32 | "previous": { 33 | "build_num": 10, 34 | "status": "retried", 35 | "build_time_millis": 0 36 | }, 37 | "status": "canceled", 38 | "committer_name": "ccie-tester", 39 | "retries": [ 40 | 12 41 | ], 42 | "subject": "Update circle.yml", 43 | "vcs_type": "github", 44 | "timedout": false, 45 | "dont_build": null, 46 | "lifecycle": "scheduled", 47 | "no_dependency_cache": null, 48 | "stop_time": "2017-10-23T05:02:09.586Z", 49 | "ssh_disabled": false, 50 | "build_time_millis": null, 51 | "picard": null, 52 | "circle_yml": { 53 | "string": "general:\n artifacts:\n - \"artifact.txt\"\n\ndependencies:\n pre:\n # fixed_bug https://circleci.atlassian.net/browse/CIRCLE-7174\n sudo service postgresql restart\n\ntest:\n override:\n - mkdir -p $CIRCLE_TEST_REPORTS/junit\n - cp test-results.xml $CIRCLE_TEST_REPORTS/junit/test-results.xml\n\n" 54 | }, 55 | "messages": [], 56 | "is_first_green_build": false, 57 | "job_name": null, 58 | "start_time": null, 59 | "canceler": { 60 | "avatar_url": "https://ghe-dev.circleci.com/avatars/u/20?", 61 | "external_id": 20, 62 | "id": 20, 63 | "name": null, 64 | "user?": true, 65 | "domain": "ghe-dev.circleci.com", 66 | "type": "github", 67 | "authorized?": true, 68 | "login": "ccie-tester" 69 | }, 70 | "all_commit_details": [ 71 | { 72 | "committer_date": "2017-10-23T00:52:25Z", 73 | "body": "", 74 | "author_date": "2017-10-23T00:52:25Z", 75 | "committer_email": "nathan+ccie-tester@circleci.com", 76 | "commit": "852a5da11d9a21cb9d272f638f51a64cd2bf2998", 77 | "committer_login": "ccie-tester", 78 | "committer_name": "ccie-tester", 79 | "subject": "Update circle.yml", 80 | "commit_url": "https://ghe-dev.circleci.com/ccie-tester/testing/commit/852a5da11d9a21cb9d272f638f51a64cd2bf2998", 81 | "author_login": "ccie-tester", 82 | "author_name": "ccie-tester", 83 | "author_email": "nathan+ccie-tester@circleci.com" 84 | } 85 | ], 86 | "platform": "1.0", 87 | "outcome": "canceled", 88 | "vcs_url": "https://ghe-dev.circleci.com/ccie-tester/testing", 89 | "author_name": "ccie-tester", 90 | "node": null, 91 | "canceled": true, 92 | "author_email": "nathan+ccie-tester@circleci.com" 93 | } 94 | -------------------------------------------------------------------------------- /tests/mocks/add_ssh_user_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "compare": null, 3 | "previous_successful_build": null, 4 | "build_parameters": null, 5 | "oss": true, 6 | "committer_date": "2017-10-23T00:52:25Z", 7 | "body": "", 8 | "fail_reason": null, 9 | "retry_of": 10, 10 | "reponame": "MOCK+testing", 11 | "ssh_users": [ 12 | { 13 | "_id": "59ec2165e8288600019ca54e", 14 | "github_id": 20, 15 | "login": "ccie-tester", 16 | "type": "github", 17 | "external_id": 20 18 | } 19 | ], 20 | "build_url": "https://ccie-preview.sphereci.com/gh/ccie-tester/testing/11", 21 | "parallel": 1, 22 | "failed": null, 23 | "branch": "master", 24 | "username": "ccie-tester", 25 | "author_date": "2017-10-23T00:52:25Z", 26 | "why": "auto-retry", 27 | "user": { 28 | "is_user": true, 29 | "login": "ccie-tester", 30 | "avatar_url": "https://ghe-dev.circleci.com/avatars/u/20?", 31 | "name": null, 32 | "vcs_type": "github", 33 | "id": 20 34 | }, 35 | "vcs_revision": "852a5da11d9a21cb9d272f638f51a64cd2bf2998", 36 | "vcs_tag": null, 37 | "build_num": 11, 38 | "infrastructure_fail": false, 39 | "committer_email": "nathan+ccie-tester@circleci.com", 40 | "previous": { 41 | "build_num": 10, 42 | "status": "retried", 43 | "build_time_millis": 0 44 | }, 45 | "status": "canceled", 46 | "committer_name": "ccie-tester", 47 | "retries": [ 48 | 12 49 | ], 50 | "subject": "Update circle.yml", 51 | "vcs_type": "github", 52 | "timedout": false, 53 | "dont_build": null, 54 | "lifecycle": "scheduled", 55 | "no_dependency_cache": null, 56 | "stop_time": "2017-10-23T05:07:44.046Z", 57 | "ssh_disabled": false, 58 | "build_time_millis": null, 59 | "picard": null, 60 | "circle_yml": { 61 | "string": "general:\n artifacts:\n - \"artifact.txt\"\n\ndependencies:\n pre:\n # fixed_bug https://circleci.atlassian.net/browse/CIRCLE-7174\n sudo service postgresql restart\n\ntest:\n override:\n - mkdir -p $CIRCLE_TEST_REPORTS/junit\n - cp test-results.xml $CIRCLE_TEST_REPORTS/junit/test-results.xml\n\n" 62 | }, 63 | "messages": [], 64 | "is_first_green_build": false, 65 | "job_name": null, 66 | "start_time": null, 67 | "canceler": { 68 | "avatar_url": "https://ghe-dev.circleci.com/avatars/u/20?", 69 | "external_id": 20, 70 | "id": 20, 71 | "name": null, 72 | "user?": true, 73 | "domain": "ghe-dev.circleci.com", 74 | "type": "github", 75 | "authorized?": true, 76 | "login": "ccie-tester" 77 | }, 78 | "all_commit_details": [ 79 | { 80 | "committer_date": "2017-10-23T00:52:25Z", 81 | "body": "", 82 | "author_date": "2017-10-23T00:52:25Z", 83 | "committer_email": "nathan+ccie-tester@circleci.com", 84 | "commit": "852a5da11d9a21cb9d272f638f51a64cd2bf2998", 85 | "committer_login": "ccie-tester", 86 | "committer_name": "ccie-tester", 87 | "subject": "Update circle.yml", 88 | "commit_url": "https://ghe-dev.circleci.com/ccie-tester/testing/commit/852a5da11d9a21cb9d272f638f51a64cd2bf2998", 89 | "author_login": "ccie-tester", 90 | "author_name": "ccie-tester", 91 | "author_email": "nathan+ccie-tester@circleci.com" 92 | } 93 | ], 94 | "platform": "1.0", 95 | "outcome": "canceled", 96 | "vcs_url": "https://ghe-dev.circleci.com/ccie-tester/testing", 97 | "author_name": "ccie-tester", 98 | "node": null, 99 | "canceled": true, 100 | "author_email": "nathan+ccie-tester@circleci.com" 101 | } 102 | -------------------------------------------------------------------------------- /tests/mocks/get_project_build_summary_response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "compare": null, 4 | "previous_successful_build": null, 5 | "build_parameters": null, 6 | "oss": true, 7 | "committer_date": "2017-10-23T00:52:25Z", 8 | "body": "", 9 | "usage_queued_at": "2017-10-23T03:13:45.341Z", 10 | "fail_reason": null, 11 | "retry_of": 3, 12 | "reponame": "testing", 13 | "ssh_users": [], 14 | "build_url": "https://ccie-preview.sphereci.com/gh/ccie-tester/testing/4", 15 | "parallel": 1, 16 | "failed": true, 17 | "branch": "master", 18 | "username": "MOCK+ccie-tester", 19 | "author_date": "2017-10-23T00:52:25Z", 20 | "why": "auto-retry", 21 | "user": { 22 | "is_user": true, 23 | "login": "ccie-tester", 24 | "avatar_url": "https://ghe-dev.circleci.com/avatars/u/20?", 25 | "name": null, 26 | "vcs_type": "github", 27 | "id": 20 28 | }, 29 | "vcs_revision": "852a5da11d9a21cb9d272f638f51a64cd2bf2998", 30 | "vcs_tag": null, 31 | "build_num": 4, 32 | "infrastructure_fail": true, 33 | "committer_email": "nathan+ccie-tester@circleci.com", 34 | "previous": { 35 | "build_num": 3, 36 | "status": "retried", 37 | "build_time_millis": 0 38 | }, 39 | "status": "infrastructure_fail", 40 | "committer_name": "ccie-tester", 41 | "retries": null, 42 | "subject": "Update circle.yml", 43 | "vcs_type": "github", 44 | "timedout": false, 45 | "dont_build": null, 46 | "lifecycle": "finished", 47 | "no_dependency_cache": null, 48 | "stop_time": "2017-10-23T03:14:23.396Z", 49 | "ssh_disabled": false, 50 | "build_time_millis": null, 51 | "picard": null, 52 | "circle_yml": { 53 | "string": "general:\n artifacts:\n - \"artifact.txt\"\n\ndependencies:\n pre:\n # fixed_bug https://circleci.atlassian.net/browse/CIRCLE-7174\n sudo service postgresql restart\n\ntest:\n override:\n - mkdir -p $CIRCLE_TEST_REPORTS/junit\n - cp test-results.xml $CIRCLE_TEST_REPORTS/junit/test-results.xml\n\n" 54 | }, 55 | "messages": [ 56 | { 57 | "type": "warning", 58 | "message": "Some errors occurred while attempting to infer information about your code." 59 | } 60 | ], 61 | "is_first_green_build": false, 62 | "job_name": null, 63 | "start_time": "2017-10-23T03:15:24.657Z", 64 | "canceler": null, 65 | "all_commit_details": [ 66 | { 67 | "committer_date": "2017-10-23T00:52:25Z", 68 | "body": "", 69 | "author_date": "2017-10-23T00:52:25Z", 70 | "committer_email": "nathan+ccie-tester@circleci.com", 71 | "commit": "852a5da11d9a21cb9d272f638f51a64cd2bf2998", 72 | "committer_login": "ccie-tester", 73 | "committer_name": "ccie-tester", 74 | "subject": "Update circle.yml", 75 | "commit_url": "https://ghe-dev.circleci.com/ccie-tester/testing/commit/852a5da11d9a21cb9d272f638f51a64cd2bf2998", 76 | "author_login": "ccie-tester", 77 | "author_name": "ccie-tester", 78 | "author_email": "nathan+ccie-tester@circleci.com" 79 | } 80 | ], 81 | "platform": "1.0", 82 | "outcome": "infrastructure_fail", 83 | "vcs_url": "https://ghe-dev.circleci.com/ccie-tester/testing", 84 | "author_name": "ccie-tester", 85 | "node": [ 86 | { 87 | "public_ip_addr": "34.215.213.111", 88 | "port": 64542, 89 | "username": "ubuntu", 90 | "image_id": null, 91 | "ssh_enabled": true 92 | } 93 | ], 94 | "queued_at": "2017-10-23T03:15:24.192Z", 95 | "canceled": false, 96 | "author_email": "nathan+ccie-tester@circleci.com" 97 | } 98 | ] 99 | -------------------------------------------------------------------------------- /tests/mocks/get_projects_response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "irc_server": null, 4 | "ssh_keys": [], 5 | "branches": { 6 | "master": { 7 | "running_builds": [ 8 | { 9 | "outcome": null, 10 | "status": "scheduled", 11 | "build_num": 3, 12 | "vcs_revision": "852a5da11d9a21cb9d272f638f51a64cd2bf2998", 13 | "pushed_at": "2017-10-23T02:43:13.536Z", 14 | "added_at": "2017-10-23T02:43:08.066Z" 15 | } 16 | ], 17 | "recent_builds": [ 18 | { 19 | "outcome": "no_tests", 20 | "status": "no_tests", 21 | "build_num": 2, 22 | "vcs_revision": "852a5da11d9a21cb9d272f638f51a64cd2bf2998", 23 | "pushed_at": "2017-10-23T02:43:13.536Z", 24 | "added_at": "2017-10-23T02:43:08.008Z" 25 | }, 26 | { 27 | "outcome": "no_tests", 28 | "status": "no_tests", 29 | "build_num": 1, 30 | "vcs_revision": "852a5da11d9a21cb9d272f638f51a64cd2bf2998", 31 | "pushed_at": "2017-10-23T02:43:13.536Z", 32 | "added_at": "2017-10-23T02:42:24.943Z" 33 | } 34 | ], 35 | "last_non_success": { 36 | "outcome": "no_tests", 37 | "status": "no_tests", 38 | "build_num": 2, 39 | "vcs_revision": "852a5da11d9a21cb9d272f638f51a64cd2bf2998", 40 | "pushed_at": "2017-10-23T02:43:13.536Z", 41 | "added_at": "2017-10-23T02:43:08.008Z" 42 | } 43 | } 44 | }, 45 | "irc_keyword": null, 46 | "oss": true, 47 | "slack_channel": null, 48 | "hipchat_notify_prefs": null, 49 | "reponame": "testing", 50 | "dependencies": "", 51 | "aws": { 52 | "keypair": null 53 | }, 54 | "slack_webhook_url": null, 55 | "irc_channel": null, 56 | "parallel": 1, 57 | "campfire_subdomain": null, 58 | "slack_integration_access_token": null, 59 | "username": "ccie-tester", 60 | "campfire_notify_prefs": null, 61 | "slack_integration_team": null, 62 | "slack_integration_channel": null, 63 | "hipchat_notify": null, 64 | "heroku_deploy_user": null, 65 | "irc_username": null, 66 | "slack_notify_prefs": null, 67 | "scopes": [ 68 | "write-settings", 69 | "view-builds", 70 | "read-settings", 71 | "trigger-builds", 72 | "all", 73 | "status", 74 | "none" 75 | ], 76 | "campfire_room": null, 77 | "hipchat_api_token": null, 78 | "campfire_token": null, 79 | "slack_subdomain": null, 80 | "has_usable_key": true, 81 | "setup": "", 82 | "vcs_type": "github", 83 | "feature_flags": { 84 | "trusty-beta": false, 85 | "osx": false, 86 | "set-github-status": true, 87 | "build-prs-only": false, 88 | "forks-receive-secret-env-vars": false, 89 | "fleet": null, 90 | "build-fork-prs": false, 91 | "autocancel-builds": false, 92 | "oss": true 93 | }, 94 | "irc_password": null, 95 | "compile": "", 96 | "slack_integration_notify_prefs": null, 97 | "slack_integration_webhook_url": null, 98 | "irc_notify_prefs": null, 99 | "slack_integration_team_id": null, 100 | "extra": "", 101 | "jira": null, 102 | "slack_integration_channel_id": null, 103 | "language": null, 104 | "hipchat_room": null, 105 | "flowdock_api_token": null, 106 | "slack_channel_override": null, 107 | "vcs_url": "MOCK+https://ghe-dev.circleci.com/ccie-tester/testing", 108 | "following": true, 109 | "default_branch": "master", 110 | "slack_api_token": null, 111 | "test": "" 112 | } 113 | ] 114 | -------------------------------------------------------------------------------- /tests/mocks/get_recent_builds_response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "compare": null, 4 | "previous_successful_build": null, 5 | "build_parameters": {}, 6 | "oss": true, 7 | "all_commit_details_truncated": false, 8 | "committer_date": "2017-10-23T00:52:25Z", 9 | "body": "", 10 | "usage_queued_at": "2017-10-23T04:37:14.638Z", 11 | "fail_reason": null, 12 | "retry_of": null, 13 | "reponame": "MOCK+testing", 14 | "ssh_users": [], 15 | "build_url": "https://ccie-preview.sphereci.com/gh/ccie-tester/testing/5", 16 | "parallel": 1, 17 | "failed": true, 18 | "branch": "master", 19 | "username": "ccie-tester", 20 | "author_date": "2017-10-23T00:52:25Z", 21 | "why": "api", 22 | "user": { 23 | "is_user": true, 24 | "login": "ccie-tester", 25 | "avatar_url": "https://ghe-dev.circleci.com/avatars/u/20?", 26 | "name": null, 27 | "vcs_type": "github", 28 | "id": 20 29 | }, 30 | "vcs_revision": "852a5da11d9a21cb9d272f638f51a64cd2bf2998", 31 | "vcs_tag": null, 32 | "build_num": 5, 33 | "infrastructure_fail": true, 34 | "committer_email": "nathan+ccie-tester@circleci.com", 35 | "previous": { 36 | "build_num": 4, 37 | "status": "infrastructure_fail", 38 | "build_time_millis": 0 39 | }, 40 | "status": "retried", 41 | "committer_name": "ccie-tester", 42 | "retries": [ 43 | 6 44 | ], 45 | "subject": "Update circle.yml", 46 | "vcs_type": "github", 47 | "timedout": false, 48 | "dont_build": null, 49 | "lifecycle": "finished", 50 | "no_dependency_cache": false, 51 | "stop_time": "2017-10-23T04:36:20.653Z", 52 | "ssh_disabled": false, 53 | "build_time_millis": null, 54 | "picard": null, 55 | "circle_yml": { 56 | "string": "general:\n artifacts:\n - \"artifact.txt\"\n\ndependencies:\n pre:\n # fixed_bug https://circleci.atlassian.net/browse/CIRCLE-7174\nsudo service postgresql restart\n\ntest:\n override:\n - mkdir -p $CIRCLE_TEST_REPORTS/junit\n - cp test-results.xml $CIRCLE_TEST_REPORTS/junit/test-results.xml\n\n" 57 | }, 58 | "messages": [ 59 | { 60 | "type": "warning", 61 | "message": "Some errors occurred while attempting to infer information about your code." 62 | } 63 | ], 64 | "is_first_green_build": false, 65 | "job_name": null, 66 | "start_time": "2017-10-23T04:37:22.053Z", 67 | "canceler": null, 68 | "all_commit_details": [ 69 | { 70 | "committer_date": "2017-10-23T00:52:25Z", 71 | "body": "", 72 | "author_date": "2017-10-23T00:52:25Z", 73 | "committer_email": "nathan+ccie-tester@circleci.com", 74 | "commit": "852a5da11d9a21cb9d272f638f51a64cd2bf2998", 75 | "committer_login": "ccie-tester", 76 | "committer_name": "ccie-tester", 77 | "subject": "Update circle.yml", 78 | "commit_url": "https://ghe-dev.circleci.com/ccie-tester/testing/commit/852a5da11d9a21cb9d272f638f51a64cd2bf2998", 79 | "author_login": "ccie-tester", 80 | "author_name": "ccie-tester", 81 | "author_email": "nathan+ccie-tester@circleci.com" 82 | } 83 | ], 84 | "platform": "1.0", 85 | "outcome": "infrastructure_fail", 86 | "vcs_url": "https://ghe-dev.circleci.com/ccie-tester/testing", 87 | "author_name": "ccie-tester", 88 | "node": [ 89 | { 90 | "public_ip_addr": "34.215.213.111", 91 | "port": 64543, 92 | "username": "ubuntu", 93 | "image_id": null, 94 | "ssh_enabled": true 95 | } 96 | ], 97 | "queued_at": "2017-10-23T04:37:14.652Z", 98 | "canceled": false, 99 | "author_email": "nathan+ccie-tester@circleci.com" 100 | } 101 | ] 102 | -------------------------------------------------------------------------------- /tests/mocks/get_build_info_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "compare": null, 3 | "previous_successful_build": null, 4 | "build_parameters": null, 5 | "oss": true, 6 | "all_commit_details_truncated": false, 7 | "committer_date": "2017-10-23T00:52:25Z", 8 | "steps": [ 9 | { 10 | "name": "Starting the build", 11 | "actions": [ 12 | { 13 | "truncated": false, 14 | "index": 0, 15 | "parallel": false, 16 | "failed": null, 17 | "infrastructure_fail": null, 18 | "name": "Starting the build", 19 | "bash_command": null, 20 | "status": "success", 21 | "timedout": null, 22 | "continue": null, 23 | "end_time": "2017-10-23T02:41:54.739Z", 24 | "type": "infrastructure", 25 | "output_url": "https://release-preview-bucket-09e3a36c.s3-us-west-2.amazonaws.com/action-logs/", 26 | "start_time": "2017-10-23T02:41:43.348Z", 27 | "background": false, 28 | "exit_code": null, 29 | "insignificant": false, 30 | "canceled": null, 31 | "step": 0, 32 | "run_time_millis": 11391, 33 | "has_output": true 34 | } 35 | ] 36 | }, 37 | { 38 | "name": "Start container", 39 | "actions": [ 40 | { 41 | "truncated": false, 42 | "index": 0, 43 | "parallel": true, 44 | "failed": null, 45 | "infrastructure_fail": null, 46 | "name": "Start container", 47 | "bash_command": null, 48 | "status": "success", 49 | "timedout": null, 50 | "continue": null, 51 | "end_time": "2017-10-23T02:42:06.225Z", 52 | "source": "config", 53 | "type": "infrastructure", 54 | "output_url": "https://release-preview-bucket-09e3a36c.s3-us-west-2.amazonaws.com/action-logs", 55 | "start_time": "2017-10-23T02:41:54.745Z", 56 | "background": false, 57 | "exit_code": 0, 58 | "insignificant": false, 59 | "canceled": null, 60 | "step": 1, 61 | "run_time_millis": 11480, 62 | "has_output": true 63 | } 64 | ] 65 | }, 66 | { 67 | "name": "Enable SSH", 68 | "actions": [ 69 | { 70 | "truncated": false, 71 | "index": 0, 72 | "parallel": true, 73 | "failed": null, 74 | "infrastructure_fail": null, 75 | "name": "Enable SSH", 76 | "bash_command": null, 77 | "status": "success", 78 | "timedout": null, 79 | "continue": null, 80 | "end_time": "2017-10-23T02:42:10.488Z", 81 | "type": "infrastructure", 82 | "output_url": "https://release-preview-bucket-09e3a36c.s3-us-west-2.amazonaws.com/action-logs", 83 | "start_time": "2017-10-23T02:42:06.231Z", 84 | "background": false, 85 | "exit_code": null, 86 | "insignificant": false, 87 | "canceled": null, 88 | "step": 2, 89 | "run_time_millis": 4257, 90 | "has_output": true 91 | } 92 | ] 93 | }, 94 | { 95 | "name": "Restore source cache", 96 | "actions": [ 97 | { 98 | "truncated": false, 99 | "index": 0, 100 | "parallel": false, 101 | "failed": null, 102 | "infrastructure_fail": null, 103 | "name": "Restore source cache", 104 | "bash_command": null, 105 | "status": "success", 106 | "timedout": null, 107 | "continue": null, 108 | "end_time": "2017-10-23T02:42:10.571Z", 109 | "source": "cache", 110 | "type": "checkout", 111 | "start_time": "2017-10-23T02:42:10.495Z", 112 | "background": false, 113 | "exit_code": null, 114 | "insignificant": false, 115 | "canceled": null, 116 | "step": 3, 117 | "run_time_millis": 76, 118 | "has_output": false 119 | } 120 | ] 121 | }, 122 | { 123 | "name": "Checkout using deploy key: 7f:0a:cb:21:e0:f2:3f:4d:d3:16:8a:88:35:16:13:2e", 124 | "actions": [ 125 | { 126 | "truncated": false, 127 | "index": 0, 128 | "parallel": true, 129 | "failed": null, 130 | "infrastructure_fail": null, 131 | "name": "Checkout using deploy key: 7f:0a:cb:21:e0:f2:3f:4d:d3:16:8a:88:35:16:13:2e", 132 | "bash_command": null, 133 | "status": "success", 134 | "timedout": null, 135 | "continue": null, 136 | "end_time": "2017-10-23T02:42:18.158Z", 137 | "source": "config", 138 | "type": "checkout", 139 | "output_url": "https://release-preview-bucket-09e3a36c.s3-us-west-2.amazonaws.com/action-logs", 140 | "start_time": "2017-10-23T02:42:10.575Z", 141 | "background": false, 142 | "exit_code": 0, 143 | "insignificant": false, 144 | "canceled": null, 145 | "step": 4, 146 | "run_time_millis": 7583, 147 | "has_output": true 148 | } 149 | ] 150 | }, 151 | { 152 | "name": "Configure the build", 153 | "actions": [ 154 | { 155 | "truncated": false, 156 | "index": 0, 157 | "parallel": false, 158 | "failed": null, 159 | "infrastructure_fail": true, 160 | "name": "Configure the build", 161 | "bash_command": null, 162 | "status": "success", 163 | "timedout": null, 164 | "continue": null, 165 | "end_time": "2017-10-23T02:42:24.853Z", 166 | "source": "cache", 167 | "type": "machine", 168 | "start_time": "2017-10-23T02:42:18.164Z", 169 | "background": false, 170 | "exit_code": null, 171 | "insignificant": false, 172 | "canceled": null, 173 | "step": 5, 174 | "run_time_millis": 6689, 175 | "has_output": false 176 | } 177 | ] 178 | } 179 | ], 180 | "body": "", 181 | "usage_queued_at": "2017-10-23T02:43:13.743Z", 182 | "fail_reason": null, 183 | "retry_of": null, 184 | "reponame": "MOCK+testing", 185 | "ssh_users": [], 186 | "build_url": "https://ccie-preview.sphereci.com/gh/ccie-tester/testing/1", 187 | "parallel": 1, 188 | "failed": true, 189 | "branch": "master", 190 | "username": "ccie-tester", 191 | "author_date": "2017-10-23T00:52:25Z", 192 | "why": "first-build", 193 | "user": { 194 | "is_user": true, 195 | "login": "ccie-tester", 196 | "avatar_url": "https://ghe-dev.circleci.com/avatars/u/20?", 197 | "name": null, 198 | "vcs_type": "github", 199 | "id": 20 200 | }, 201 | "vcs_revision": "852a5da11d9a21cb9d272f638f51a64cd2bf2998", 202 | "owners": [ 203 | "ccie-tester" 204 | ], 205 | "vcs_tag": null, 206 | "pull_requests": [], 207 | "build_num": 1, 208 | "infrastructure_fail": true, 209 | "committer_email": "nathan+ccie-tester@circleci.com", 210 | "previous": null, 211 | "status": "retried", 212 | "committer_name": "ccie-tester", 213 | "retries": [ 214 | 2 215 | ], 216 | "subject": "Update circle.yml", 217 | "vcs_type": "github", 218 | "timedout": false, 219 | "dont_build": null, 220 | "lifecycle": "finished", 221 | "no_dependency_cache": false, 222 | "stop_time": "2017-10-23T02:42:24.925Z", 223 | "ssh_disabled": false, 224 | "build_time_millis": null, 225 | "picard": null, 226 | "circle_yml": { 227 | "string": "general:\n artifacts:\n - \"artifact.txt\"\n\ndependencies:\n pre:\n # fixed_bug https://circleci.atlassian.net/browse/CIRCLE-7174\n sudo service postgresql restart\n\ntest:\n override:\n - mkdir -p $CIRCLE_TEST_REPORTS/junit\n - cp test-results.xml $CIRCLE_TEST_REPORTS/junit/test-results.xml\n\n" 228 | }, 229 | "messages": [ 230 | { 231 | "type": "warning", 232 | "message": "Some errors occurred while attempting to infer information about your code." 233 | } 234 | ], 235 | "is_first_green_build": false, 236 | "job_name": null, 237 | "start_time": "2017-10-23T02:43:21.487Z", 238 | "canceler": null, 239 | "all_commit_details": [ 240 | { 241 | "committer_date": "2017-10-23T00:52:25Z", 242 | "body": "", 243 | "author_date": "2017-10-23T00:52:25Z", 244 | "committer_email": "nathan+ccie-tester@circleci.com", 245 | "commit": "852a5da11d9a21cb9d272f638f51a64cd2bf2998", 246 | "committer_login": "ccie-tester", 247 | "committer_name": "ccie-tester", 248 | "subject": "Update circle.yml", 249 | "commit_url": "https://ghe-dev.circleci.com/ccie-tester/testing/commit/852a5da11d9a21cb9d272f638f51a64cd2bf2998", 250 | "author_login": "ccie-tester", 251 | "author_name": "ccie-tester", 252 | "author_email": "nathan+ccie-tester@circleci.com" 253 | } 254 | ], 255 | "platform": "1.0", 256 | "outcome": "infrastructure_fail", 257 | "vcs_url": "https://ghe-dev.circleci.com/ccie-tester/testing", 258 | "author_name": "ccie-tester", 259 | "node": [ 260 | { 261 | "public_ip_addr": "34.215.213.111", 262 | "port": 64539, 263 | "username": "ubuntu", 264 | "image_id": null, 265 | "ssh_enabled": true 266 | } 267 | ], 268 | "queued_at": "2017-10-23T02:43:14.019Z", 269 | "canceled": false, 270 | "author_email": "nathan+ccie-tester@circleci.com" 271 | } 272 | -------------------------------------------------------------------------------- /tests/test_api.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pytest 3 | from unittest.mock import MagicMock 4 | 5 | from pycircleci.api import Api, CircleciError, DELETE, GET, POST, PUT 6 | 7 | TEST_ID = "deadbeef-dead-beef-dead-deaddeafbeef" 8 | 9 | 10 | @pytest.fixture(scope="session") 11 | def cci(): 12 | """Initialize a CircleCI API client""" 13 | return Api("TOKEN") 14 | 15 | 16 | def get_mock(api_client, filename): 17 | """Get a mock response from file""" 18 | filename = f"tests/mocks/{filename}" 19 | with open(filename, "r") as f: 20 | text = f.read() 21 | resp = json.loads(text) 22 | api_client._request = MagicMock(return_value=resp) 23 | # Spy on this, but don't mock its return value 24 | api_client._request_get_items = MagicMock(wraps=api_client._request_get_items) 25 | 26 | 27 | def assert_message_accepted(resp): 28 | assert "Accepted" in resp["message"] 29 | 30 | 31 | def assert_message_ok(resp): 32 | assert resp["message"] == "ok" 33 | 34 | 35 | def test_invalid_http_method(cci): 36 | with pytest.raises(CircleciError) as ex: 37 | cci._request("BAD", "dummy") 38 | assert "Invalid HTTP method: BAD" in str(ex.value) 39 | 40 | 41 | def test_get_user_info(cci): 42 | get_mock(cci, "get_user_info_response.json") 43 | resp = cci.get_user_info() 44 | assert resp["selected_email"] == "mock+ccie-tester@circleci.com" 45 | 46 | 47 | def test_get_user_id_info(cci): 48 | get_mock(cci, "get_user_id_info_response.json") 49 | resp = cci.get_user_id_info(TEST_ID) 50 | assert resp["id"] == TEST_ID 51 | assert resp["name"] == "John" 52 | assert resp["login"] == "johndoe" 53 | 54 | 55 | def test_get_user_collaborations(cci): 56 | get_mock(cci, "get_user_collaborations_response.json") 57 | resp = cci.get_user_collaborations() 58 | assert resp[0]["vcs_type"] == "github" 59 | assert resp[0]["name"] == "johndoe" 60 | assert resp[1]["vcs_type"] == "github" 61 | assert resp[1]["name"] == "org1" 62 | 63 | 64 | def test_get_user_repos(cci): 65 | get_mock(cci, "get_user_repos_response.json") 66 | resp = cci.get_user_repos() 67 | cci._request_get_items.assert_called_once_with( 68 | "/user/repos/github", 69 | api_version="v1.1", 70 | paginate=False, 71 | limit=None, 72 | ) 73 | assert len(resp) == 3 74 | assert resp[0]["vcs_type"] == "github" 75 | assert resp[0]["name"] == "repo1" 76 | assert resp[0]["username"] == "foobar" 77 | assert resp[0]["has_followers"] is False 78 | assert resp[1]["vcs_type"] == "github" 79 | assert resp[1]["name"] == "repo2" 80 | assert resp[1]["has_followers"] is True 81 | assert resp[2]["username"] == "otherorg" 82 | assert resp[2]["owner"]["login"] == "otherorg" 83 | assert resp[2]["has_followers"] is False 84 | 85 | 86 | def test_get_user_repos_limit(cci): 87 | get_mock(cci, "get_user_repos_response.json") 88 | resp = cci.get_user_repos(limit=2) 89 | assert cci._request_get_items.call_args.args[0] == "/user/repos/github" 90 | assert len(resp) == 2 91 | 92 | 93 | def test_get_project(cci): 94 | get_mock(cci, "get_project_response.json") 95 | resp = cci.get_project("gh/foo/bar") 96 | assert resp["slug"] == "gh/foo/bar" 97 | assert resp["organization_name"] == "foo" 98 | assert resp["name"] == "bar" 99 | assert "vcs_info" in resp 100 | 101 | 102 | def test_get_projects(cci): 103 | get_mock(cci, "get_projects_response.json") 104 | resp = cci.get_projects() 105 | assert resp[0]["vcs_url"] == "MOCK+https://ghe-dev.circleci.com/ccie-tester/testing" 106 | 107 | 108 | def test_follow_project(cci): 109 | get_mock(cci, "follow_project_response.json") 110 | resp = cci.follow_project("ccie-tester", "testing") 111 | assert resp["mock+following"] is True 112 | 113 | 114 | def test_get_project_build_summary(cci): 115 | get_mock(cci, "get_project_build_summary_response.json") 116 | resp = cci.get_project_build_summary("ccie-tester", "testing") 117 | assert resp[0]["username"] == "MOCK+ccie-tester" 118 | 119 | # with invalid status filter 120 | with pytest.raises(CircleciError) as ex: 121 | cci.get_project_build_summary("ccie-tester", "testing", status_filter="bad") 122 | assert "Invalid status: bad" in str(ex.value) 123 | 124 | # with branch 125 | resp = cci.get_project_build_summary("ccie-tester", "testing", branch="master") 126 | assert resp[0]["username"] == "MOCK+ccie-tester" 127 | 128 | 129 | def test_get_recent_builds(cci): 130 | get_mock(cci, "get_recent_builds_response.json") 131 | resp = cci.get_recent_builds() 132 | assert resp[0]["reponame"] == "MOCK+testing" 133 | 134 | 135 | def test_get_build_info(cci): 136 | get_mock(cci, "get_build_info_response.json") 137 | resp = cci.get_build_info("ccie-tester", "testing", "1") 138 | assert resp["reponame"] == "MOCK+testing" 139 | 140 | 141 | def test_get_artifacts(cci): 142 | get_mock(cci, "get_artifacts_response.json") 143 | resp = cci.get_artifacts("ccie-tester", "testing", "1") 144 | assert resp[0]["path"] == "MOCK+raw-test-output/go-test-report.xml" 145 | 146 | 147 | def test_retry_build(cci): 148 | get_mock(cci, "retry_build_response.json") 149 | resp = cci.retry_build("ccie-tester", "testing", "1") 150 | assert resp["reponame"] == "MOCK+testing" 151 | 152 | # with SSH 153 | resp = cci.retry_build("ccie-tester", "testing", "1", ssh=True) 154 | assert resp["reponame"] == "MOCK+testing" 155 | 156 | 157 | def test_cancel_build(cci): 158 | get_mock(cci, "cancel_build_response.json") 159 | resp = cci.cancel_build("ccie-tester", "testing", "11") 160 | assert resp["reponame"] == "MOCK+testing" 161 | assert resp["build_num"] == 11 162 | assert resp["canceled"] is True 163 | 164 | 165 | def test_add_ssh_user(cci): 166 | get_mock(cci, "add_ssh_user_response.json") 167 | resp = cci.add_ssh_user("ccie-tester", "testing", "11") 168 | assert resp["reponame"] == "MOCK+testing" 169 | assert resp["ssh_users"][0]["login"] == "ccie-tester" 170 | 171 | 172 | def test_trigger_build(cci): 173 | get_mock(cci, "trigger_build_response.json") 174 | resp = cci.trigger_build("ccie-tester", "testing") 175 | assert resp["reponame"] == "MOCK+testing" 176 | 177 | 178 | def test_trigger_pipeline(cci): 179 | get_mock(cci, "trigger_pipeline_response.json") 180 | resp = cci.trigger_pipeline("ccie-tester", "testing") 181 | assert resp["state"] == "pending" 182 | 183 | 184 | def test_get_project_pipelines_depaginated(cci): 185 | get_mock(cci, "get_project_pipelines_response.json") 186 | resp = cci.get_project_pipelines("foo", "bar") 187 | assert resp[0]["project_slug"] == "gh/foo/bar" 188 | 189 | 190 | def test_get_project_pipeline(cci): 191 | get_mock(cci, "get_project_pipeline_response.json") 192 | resp = cci.get_project_pipeline("foo", "bar", 1234) 193 | assert resp["number"] == 1234 194 | 195 | 196 | def test_continue_pipeline(cci): 197 | get_mock(cci, "message_accepted_response.json") 198 | resp = cci.continue_pipeline("continuation_key", "config") 199 | assert_message_accepted(resp) 200 | 201 | 202 | def test_get_pipelines(cci): 203 | get_mock(cci, "get_pipelines_response.json") 204 | resp = cci.get_pipelines("foo") 205 | assert resp[0]["project_slug"] == "gh/foo/bar" 206 | 207 | 208 | def test_get_pipeline(cci): 209 | get_mock(cci, "get_pipeline_response.json") 210 | resp = cci.get_pipeline(TEST_ID) 211 | assert resp["state"] == "created" 212 | 213 | 214 | def test_get_pipeline_config(cci): 215 | get_mock(cci, "get_pipeline_config_response.json") 216 | resp = cci.get_pipeline_config(TEST_ID) 217 | assert "source" in resp 218 | assert "compiled" in resp 219 | 220 | 221 | def test_get_pipeline_workflow_depaginated(cci): 222 | get_mock(cci, "get_pipeline_workflow_response.json") 223 | resp = cci.get_pipeline_workflow(TEST_ID) 224 | assert resp[0]["project_slug"] == "gh/foo/bar" 225 | 226 | 227 | def test_get_workflow(cci): 228 | get_mock(cci, "get_workflow_response.json") 229 | resp = cci.get_workflow(TEST_ID) 230 | assert resp["status"] == "running" 231 | 232 | 233 | def test_get_workflow_jobs_depaginated(cci): 234 | get_mock(cci, "get_workflow_jobs_response.json") 235 | resp = cci.get_workflow_jobs(TEST_ID) 236 | assert len(resp) == 2 237 | 238 | 239 | def test_approve_job(cci): 240 | get_mock(cci, "message_accepted_response.json") 241 | resp = cci.approve_job("workflow_id", "approval_request_id") 242 | assert_message_accepted(resp) 243 | 244 | 245 | def test_list_checkout_keys(cci): 246 | get_mock(cci, "list_checkout_keys_response.json") 247 | resp = cci.list_checkout_keys("user", "circleci-sandbox") 248 | assert resp[0]["type"] == "deploy-key" 249 | assert "public_key" in resp[0] 250 | 251 | 252 | def test_create_checkout_key(cci): 253 | with pytest.raises(CircleciError) as ex: 254 | cci.create_checkout_key("user", "test", "bad") 255 | assert "Invalid key type: bad" in str(ex.value) 256 | 257 | get_mock(cci, "create_checkout_key_response.json") 258 | resp = cci.create_checkout_key("user", "test", "deploy-key") 259 | assert resp["type"] == "deploy-key" 260 | assert "public_key" in resp 261 | 262 | 263 | def test_get_checkout_key(cci): 264 | get_mock(cci, "get_checkout_key_response.json") 265 | resp = cci.get_checkout_key( 266 | "user", 267 | "circleci-sandbox", 268 | "94:19:ab:a9:f4:2b:21:1c:a5:87:dd:ee:3d:c2:90:4e", 269 | ) 270 | assert resp["type"] == "deploy-key" 271 | assert "public_key" in resp 272 | 273 | 274 | def test_delete_checkout_key(cci): 275 | get_mock(cci, "message_ok_response.json") 276 | resp = cci.delete_checkout_key( 277 | "user", 278 | "circleci-sandbox", 279 | "94:19:ab:a9:f4:2b:21:1c:a5:87:dd:ee:3d:c2:90:4e", 280 | ) 281 | assert_message_ok(resp) 282 | 283 | 284 | def test_get_test_metadata(cci): 285 | get_mock(cci, "get_test_metadata_response.json") 286 | resp = cci.get_test_metadata("user", "circleci-demo-javascript-express", 127) 287 | assert len(resp) == 2 288 | assert "tests" in resp 289 | 290 | 291 | def test_list_envvars(cci): 292 | get_mock(cci, "list_envvars_response.json") 293 | resp = cci.list_envvars("user", "circleci-sandbox") 294 | assert len(resp) == 4 295 | assert resp[0]["name"] == "BAR" 296 | 297 | 298 | def test_add_envvar(cci): 299 | get_mock(cci, "get_envvar_response.json") 300 | resp = cci.add_envvar("user", "circleci-sandbox", "foo", "bar") 301 | assert resp["name"] == "foo" 302 | assert resp["value"] != "bar" 303 | 304 | 305 | def test_get_envvar(cci): 306 | get_mock(cci, "get_envvar_response.json") 307 | resp = cci.get_envvar("user", "circleci-sandbox", "foo") 308 | assert resp["name"] == "foo" 309 | assert resp["value"] != "bar" 310 | 311 | 312 | def test_delete_envvar(cci): 313 | get_mock(cci, "message_ok_response.json") 314 | resp = cci.delete_envvar("user", "circleci-sandbox", "foo") 315 | assert_message_ok(resp) 316 | 317 | 318 | def test_get_contexts_depaginated(cci): 319 | get_mock(cci, "get_contexts_response.json") 320 | resp = cci.get_contexts("user") 321 | cci._request_get_items.assert_called_once_with( 322 | "context", 323 | params={ 324 | "owner-type": "organization", 325 | "owner-slug": "github/user", 326 | }, 327 | paginate=False, 328 | limit=None, 329 | ) 330 | assert resp[0]["id"] == TEST_ID 331 | assert resp[0]["name"] == "context1" 332 | assert resp[2]["name"] == "foobar" 333 | 334 | 335 | def test_get_contexts_owner_id(cci): 336 | get_mock(cci, "get_contexts_response.json") 337 | resp = cci.get_contexts(owner_id=TEST_ID) 338 | cci._request_get_items.assert_called_once_with( 339 | "context", 340 | params={ 341 | "owner-type": "organization", 342 | "owner-id": TEST_ID, 343 | }, 344 | paginate=False, 345 | limit=None, 346 | ) 347 | assert resp[0]["name"] == "context1" 348 | 349 | 350 | def test_get_contexts_owner_type(cci): 351 | get_mock(cci, "get_contexts_response.json") 352 | resp = cci.get_contexts("user", owner_type="account") 353 | cci._request_get_items.assert_called_once_with( 354 | "context", 355 | params={ 356 | "owner-type": "account", 357 | "owner-slug": "github/user", 358 | }, 359 | paginate=False, 360 | limit=None, 361 | ) 362 | assert resp[0]["name"] == "context1" 363 | 364 | 365 | def test_add_context(cci): 366 | get_mock(cci, "get_context_response.json") 367 | resp = cci.add_context("testcontext", "user") 368 | cci._request.assert_called_once_with( 369 | POST, 370 | "context", 371 | data={ 372 | "name": "testcontext", 373 | "owner": { 374 | "type": "organization", 375 | "slug": "github/user", 376 | }, 377 | }, 378 | api_version="v2", 379 | ) 380 | assert resp["name"] == "testcontext" 381 | 382 | 383 | def test_add_context_organization(cci): 384 | get_mock(cci, "get_context_response.json") 385 | resp = cci.add_context("testcontext", "user", owner_type="account") 386 | cci._request.assert_called_once_with( 387 | POST, 388 | "context", 389 | data={ 390 | "name": "testcontext", 391 | "owner": { 392 | "type": "account", 393 | "slug": "github/user", 394 | }, 395 | }, 396 | api_version="v2", 397 | ) 398 | assert resp["name"] == "testcontext" 399 | 400 | 401 | def test_get_context(cci): 402 | get_mock(cci, "get_context_response.json") 403 | resp = cci.get_context(TEST_ID) 404 | cci._request.assert_called_once_with( 405 | GET, 406 | f"context/{TEST_ID}", 407 | api_version="v2", 408 | ) 409 | assert resp["name"] == "testcontext" 410 | 411 | 412 | def test_delete_context(cci): 413 | get_mock(cci, "delete_context_response.json") 414 | resp = cci.delete_context(TEST_ID) 415 | cci._request.assert_called_once_with( 416 | DELETE, 417 | f"context/{TEST_ID}", 418 | api_version="v2", 419 | ) 420 | assert resp["message"] == "Context deleted." 421 | 422 | 423 | def test_get_context_envvars_depaginated(cci): 424 | get_mock(cci, "get_context_envvars_response.json") 425 | resp = cci.get_context_envvars(TEST_ID) 426 | cci._request_get_items.assert_called_once_with( 427 | f"context/{TEST_ID}/environment-variable", 428 | paginate=False, 429 | limit=None, 430 | ) 431 | assert resp[1]["variable"] == "FOOBAR" 432 | assert resp[2]["variable"] == "FOOBAR2" 433 | 434 | 435 | def test_add_context_envvar(cci): 436 | get_mock(cci, "add_context_envvar_response.json") 437 | resp = cci.add_context_envvar(TEST_ID, "FOOBAR", "BAZ") 438 | cci._request.assert_called_once_with( 439 | PUT, 440 | f"context/{TEST_ID}/environment-variable/FOOBAR", 441 | api_version="v2", 442 | data={"value": "BAZ"}, 443 | ) 444 | assert resp["variable"] == "FOOBAR" 445 | 446 | 447 | def test_delete_context_envvar(cci): 448 | get_mock(cci, "delete_context_envvar_response.json") 449 | resp = cci.delete_context_envvar(TEST_ID, "FOOBAR") 450 | cci._request.assert_called_once_with( 451 | DELETE, 452 | f"context/{TEST_ID}/environment-variable/FOOBAR", 453 | api_version="v2", 454 | ) 455 | assert resp["message"] == "Environment variable deleted." 456 | 457 | 458 | def test_get_latest_artifact(cci): 459 | get_mock(cci, "get_latest_artifacts_response.json") 460 | resp = cci.get_latest_artifact("user", "circleci-sandbox") 461 | assert resp[0]["path"] == "circleci-docs/index.html" 462 | 463 | resp = cci.get_latest_artifact("user", "circleci-sandbox", "master") 464 | assert resp[0]["path"] == "circleci-docs/index.html" 465 | 466 | with pytest.raises(CircleciError) as ex: 467 | cci.get_latest_artifact("user", "circleci-sandbox", "master", "bad") 468 | assert "Invalid status: bad" in str(ex.value) 469 | 470 | 471 | def test_get_project_settings(cci): 472 | get_mock(cci, "get_project_settings_response.json") 473 | resp = cci.get_project_settings("user", "circleci-sandbox") 474 | assert resp["default_branch"] == "master" 475 | 476 | 477 | def test_get_project_branches(cci): 478 | get_mock(cci, "get_project_branches_response.json") 479 | resp = cci.get_project_branches("foo", "bar") 480 | assert "master" in resp["branches"] 481 | 482 | 483 | def test_get_flaky_tests(cci): 484 | get_mock(cci, "get_flaky_tests_response.json") 485 | resp = cci.get_flaky_tests("foo", "bar") 486 | assert resp["total_flaky_tests"] == 2 487 | assert len(resp["flaky_tests"]) == 2 488 | assert resp["flaky_tests"][0]["times_flaked"] == 10 489 | 490 | 491 | def test_get_project_workflows_metrics_depaginated(cci): 492 | get_mock(cci, "get_project_workflows_metrics_response.json") 493 | resp = cci.get_project_workflows_metrics("foo", "bar") 494 | assert "metrics" in resp[0] 495 | assert "duration_metrics" in resp[0]["metrics"] 496 | 497 | 498 | def test_get_project_workflow_metrics_depaginated(cci): 499 | get_mock(cci, "get_project_workflow_metrics_response.json") 500 | resp = cci.get_project_workflow_metrics("foo", "bar", "workflow") 501 | assert resp[0]["status"] == "success" 502 | assert "duration" in resp[0] 503 | 504 | 505 | def test_get_project_workflow_test_metrics(cci): 506 | get_mock(cci, "get_project_workflow_test_metrics_response.json") 507 | resp = cci.get_project_workflow_test_metrics("foo", "bar", "workflow") 508 | assert resp["average_test_count"] == 2 509 | assert resp["total_test_runs"] == 3 510 | 511 | 512 | def test_get_project_workflow_jobs_metrics_depaginated(cci): 513 | get_mock(cci, "get_project_workflow_jobs_metrics_response.json") 514 | resp = cci.get_project_workflow_jobs_metrics("foo", "bar", "workflow") 515 | assert "metrics" in resp[0] 516 | assert "duration_metrics" in resp[0]["metrics"] 517 | 518 | 519 | def test_get_project_workflow_job_metrics_depaginated(cci): 520 | get_mock(cci, "get_project_workflow_job_metrics_response.json") 521 | resp = cci.get_project_workflow_job_metrics("foo", "bar", "workflow", "job") 522 | assert resp[0]["status"] == "success" 523 | assert "duration" in resp[0] 524 | 525 | 526 | def test_get_schedules(cci): 527 | get_mock(cci, "get_schedules_response.json") 528 | resp = cci.get_schedules("foo", "bar") 529 | assert resp[0]["project-slug"] == "gh/foo/bar" 530 | assert resp[0]["name"] == "schedule1" 531 | 532 | 533 | def test_get_schedule(cci): 534 | get_mock(cci, "get_schedule_response.json") 535 | resp = cci.get_schedule(TEST_ID) 536 | assert resp["project-slug"] == "gh/foo/bar" 537 | assert resp["name"] == "schedule1" 538 | 539 | 540 | def test_add_schedule(cci): 541 | get_mock(cci, "get_schedule_response.json") 542 | data = { 543 | "name": "schedule1", 544 | "timetable": {"per-hour": 0, "hours-of-day": [0], "days-of-week": ["TUE"]}, 545 | "attribution-actor": "current", 546 | "parameters": {"deploy_prod": True, "branch": "new_feature"}, 547 | } 548 | resp = cci.add_schedule("foo", "bar", "schedule1", settings=data) 549 | assert resp["project-slug"] == "gh/foo/bar" 550 | assert resp["name"] == "schedule1" 551 | 552 | 553 | def test_update_schedule(cci): 554 | get_mock(cci, "get_schedule_response.json") 555 | data = {"description": "test schedule"} 556 | resp = cci.update_schedule(TEST_ID, data) 557 | assert resp["description"] == "test schedule" 558 | 559 | 560 | def test_delete_schedule(cci): 561 | get_mock(cci, "message_accepted_response.json") 562 | resp = cci.delete_schedule(TEST_ID) 563 | assert_message_accepted(resp) 564 | 565 | 566 | def test_get_job_details(cci): 567 | get_mock(cci, "get_job_details_response.json") 568 | resp = cci.get_job_details("foo", "bar", 12345) 569 | assert resp["project"]["slug"] == "gh/foo/bar" 570 | assert resp["number"] == 12345 571 | 572 | 573 | def test_cancel_job(cci): 574 | get_mock(cci, "message_accepted_response.json") 575 | resp = cci.cancel_job("foo", "bar", 12345) 576 | assert_message_accepted(resp) 577 | 578 | 579 | def test_cancel_workflow(cci): 580 | get_mock(cci, "message_accepted_response.json") 581 | resp = cci.cancel_workflow(TEST_ID) 582 | assert_message_accepted(resp) 583 | 584 | 585 | def test_rerun_workflow(cci): 586 | get_mock(cci, "message_accepted_response.json") 587 | resp = cci.rerun_workflow(TEST_ID, from_failed=True) 588 | assert_message_accepted(resp) 589 | -------------------------------------------------------------------------------- /pycircleci/api.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | import requests 5 | from requests.adapters import HTTPAdapter 6 | from requests.auth import HTTPBasicAuth 7 | from requests_toolbelt.utils import dump 8 | from urllib3 import Retry 9 | 10 | API_BASE_URL = "https://circleci.com/api" 11 | 12 | CIRCLE_TOKEN = os.getenv("CIRCLE_TOKEN") 13 | CIRCLE_API_URL = os.getenv("CIRCLE_API_URL", API_BASE_URL) 14 | CIRCLE_API_KEY_HEADER = "Circle-Token" 15 | 16 | API_VER_V1 = "v1.1" 17 | API_VER_V2 = "v2" 18 | API_VERSIONS = [API_VER_V1, API_VER_V2] 19 | 20 | DELETE, GET, PATCH, POST, PUT = HTTP_METHODS = ["DELETE", "GET", "PATCH", "POST", "PUT"] 21 | 22 | BITBUCKET = "bitbucket" # bb 23 | GITHUB = "github" # gh 24 | ORG = "organization" 25 | 26 | 27 | class CircleciError(Exception): 28 | pass 29 | 30 | 31 | class Api: 32 | """Client for CircleCI API""" 33 | 34 | def __init__(self, token=None, url=None): 35 | """Initialize a client to interact with CircleCI API. 36 | 37 | :param token: CircleCI API access token. Defaults to CIRCLE_TOKEN env var 38 | :param url: The URL of the CircleCI API instance. 39 | Defaults to https://circleci.com/api. If running a self-hosted 40 | CircleCI server, the API is available at the ``/api`` endpoint of the 41 | installation url, i.e. https://circleci.yourcompany.com/api 42 | """ 43 | url = CIRCLE_API_URL if url is None else url 44 | token = CIRCLE_TOKEN if token is None else token 45 | if not token: 46 | raise CircleciError("Missing or empty CircleCI API access token") 47 | 48 | self.token = token 49 | self.url = url 50 | self._session = self._request_session() 51 | self.last_response = None 52 | 53 | def __repr__(self): 54 | opts = {"token": self.token, "url": self.url} 55 | kwargs = [f"{k}={v!r}" for k, v in opts.items()] 56 | return f'Api({", ".join(kwargs)})' 57 | 58 | def ppj(self, data): 59 | """Pretty print data as json""" 60 | print(json.dumps(data, indent=2)) 61 | 62 | def ppr(self, resp=None): 63 | """Pretty print the last request/response details""" 64 | resp = self.last_response if resp is None else resp 65 | if resp: 66 | data = dump.dump_all(resp) 67 | print(data.decode("utf-8", "ignore")) 68 | 69 | def get_user_info(self, api_version=None): 70 | """Get info about the signed in user. 71 | 72 | :param api_version: Optional API version. Defaults to v1.1 73 | 74 | Endpoint: 75 | GET ``/me`` 76 | """ 77 | endpoint = "me" 78 | resp = self._request(GET, endpoint, api_version=api_version) 79 | return resp 80 | 81 | # alias for get_user_info() 82 | me = get_user_info 83 | 84 | def get_user_id_info(self, user_id): 85 | """Get info about the user with a given ID. 86 | 87 | Endpoint: 88 | GET ``/user/:user-id`` 89 | """ 90 | endpoint = f"user/{user_id}" 91 | resp = self._request(GET, endpoint, api_version=API_VER_V2) 92 | return resp 93 | 94 | def get_user_collaborations(self): 95 | """Get the set of organizations of which a user is a member or a collaborator. 96 | 97 | Endpoint: 98 | GET ``/me/collaborations`` 99 | """ 100 | endpoint = "me/collaborations" 101 | resp = self._request(GET, endpoint, api_version=API_VER_V2) 102 | return resp 103 | 104 | def get_user_repos(self, vcs_type=GITHUB, paginate=False, limit=None): 105 | """Get list of repos accessible to the user. 106 | 107 | .. note:: 108 | This is useful because ``get_projects`` only shows projects a user 109 | is following, and ``get_project`` can show a project as existing 110 | even if it isn't currently setup. 111 | 112 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 113 | :param paginate: If True, repeatedly requests more items from the endpoint until the limit has been reached (or until all results have been fetched). Defaults to False. 114 | :param limit: Maximum number of items to return. By default returns all the results from multiple calls to the endpoint, or all the results from a single call to the endpoint, depending on the value for ``paginate``. 115 | 116 | Endpoint: 117 | GET ``/user/repos/:vcs-type`` 118 | """ 119 | endpoint = f"/user/repos/{vcs_type}" 120 | 121 | resp = self._request_get_items(endpoint, api_version=API_VER_V1, paginate=paginate, limit=limit) 122 | return resp 123 | 124 | def get_project(self, slug): 125 | """Get a project by project slug. 126 | 127 | :param slug: Project slug. 128 | 129 | Endpoint: 130 | GET ``/project/:slug`` 131 | """ 132 | endpoint = f"project/{slug}" 133 | resp = self._request(GET, endpoint, api_version=API_VER_V2) 134 | return resp 135 | 136 | def get_projects(self): 137 | """Get list of projects followed. 138 | 139 | Endpoint: 140 | GET ``/projects`` 141 | """ 142 | endpoint = "projects" 143 | resp = self._request(GET, endpoint) 144 | return resp 145 | 146 | def follow_project(self, username, project, vcs_type=GITHUB): 147 | """Follow a project. 148 | 149 | :param username: Org or user name. 150 | :param project: Repo name. 151 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 152 | 153 | Endpoint: 154 | POST ``/project/:vcs-type/:username/:project/follow`` 155 | """ 156 | slug = self.project_slug(username, project, vcs_type) 157 | endpoint = f"project/{slug}/follow" 158 | resp = self._request(POST, endpoint) 159 | return resp 160 | 161 | def get_project_build_summary( 162 | self, 163 | username, 164 | project, 165 | limit=30, 166 | offset=0, 167 | status_filter=None, 168 | branch=None, 169 | vcs_type=GITHUB, 170 | shallow=False, 171 | ): 172 | """Get build summary for each of the last 30 builds for a single repo. 173 | 174 | :param username: Org or user name. 175 | :param project: Repo name. 176 | :param limit: Number of builds to return. Maximum 100, defaults to 30. 177 | :param offset: The API returns builds starting from this offset, defaults to 0. 178 | :param status_filter: Restricts which builds are returned. 179 | Set to "completed", "successful", "running" or "failed". 180 | Defaults to None (no filter). 181 | :param branch: Restricts returned builds to a single branch. 182 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 183 | :param shallow: Optional boolean value that may be sent to improve 184 | overall performance if set to "true". 185 | 186 | :type limit: int 187 | :type offset: int 188 | :type shallow: bool 189 | 190 | Endpoint: 191 | GET ``/project/:vcs-type/:username/:project`` 192 | """ 193 | valid_filters = ["completed", "successful", "failed", "running", None] 194 | if status_filter not in valid_filters: 195 | raise CircleciError(f"Invalid status: {status_filter}. Valid values are: {valid_filters}") 196 | 197 | params = {"limit": limit, "offset": offset} 198 | 199 | if status_filter: 200 | params["filter"] = status_filter 201 | if shallow: 202 | params["shallow"] = True 203 | 204 | slug = self.project_slug(username, project, vcs_type) 205 | endpoint = f"project/{slug}" 206 | if branch: 207 | endpoint += f"/tree/{branch}" 208 | resp = self._request(GET, endpoint, params=params) 209 | return resp 210 | 211 | def get_recent_builds(self, limit=30, offset=0, shallow=False): 212 | """Get build summary for each of the last 30 recent builds, ordered by build_num. 213 | 214 | :param limit: Number of builds to return. Maximum 100, defaults to 30. 215 | :param offset: The API returns builds starting from this offset, defaults to 0. 216 | :param shallow: Optional boolean value that may be sent to improve 217 | overall performance if set to "true". 218 | 219 | :type limit: int 220 | :type offset: int 221 | :type shallow: bool 222 | 223 | Endpoint: 224 | GET ``/recent-builds`` 225 | """ 226 | params = {"limit": limit, "offset": offset} 227 | 228 | if shallow: 229 | params["shallow"] = True 230 | 231 | endpoint = "recent-builds" 232 | resp = self._request(GET, endpoint, params=params) 233 | return resp 234 | 235 | def get_build_info(self, username, project, build_num, vcs_type=GITHUB): 236 | """Get full details of a single build. 237 | 238 | :param username: Org or user name. 239 | :param project: Repo name. 240 | :param build_num: Build number. 241 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 242 | 243 | Endpoint: 244 | GET ``/project/:vcs-type/:username/:project/:build-num`` 245 | """ 246 | slug = self.project_slug(username, project, vcs_type) 247 | endpoint = f"project/{slug}/{build_num}" 248 | resp = self._request(GET, endpoint) 249 | return resp 250 | 251 | def get_artifacts(self, username, project, build_num, vcs_type=GITHUB): 252 | """Get list of artifacts produced by a given build. 253 | 254 | :param username: Org or user name. 255 | :param project: Repo name. 256 | :param build_num: Build number. 257 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 258 | 259 | Endpoint: 260 | GET ``/project/:vcs-type/:username/:project/:build-num/artifacts`` 261 | """ 262 | slug = self.project_slug(username, project, vcs_type) 263 | endpoint = f"project/{slug}/{build_num}/artifacts" 264 | resp = self._request(GET, endpoint) 265 | return resp 266 | 267 | def get_latest_artifact( 268 | self, 269 | username, 270 | project, 271 | branch=None, 272 | status_filter="completed", 273 | vcs_type=GITHUB, 274 | ): 275 | """Get list of artifacts produced by the latest build on a given branch. 276 | 277 | .. note:: 278 | This endpoint is a little bit flakey. If the "latest" 279 | build does not have any artifacts, rather than returning 280 | an empty set, the API returns a 404. 281 | 282 | :param username: Org or user name. 283 | :param project: Repo name. 284 | :param branch: The branch to look in for the latest build. 285 | Returns artifacts for latest build in the entire project if omitted. 286 | :param filter: Restricts which builds are returned. Defaults to "completed". 287 | Valid filters: "completed", "successful", "failed" 288 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 289 | 290 | Endpoint: 291 | GET ``/project/:vcs-type/:username/:project/latest/artifacts`` 292 | """ 293 | valid_filters = ["completed", "successful", "failed"] 294 | if status_filter not in valid_filters: 295 | raise CircleciError(f"Invalid status: {status_filter}. Valid values are: {valid_filters}") 296 | 297 | params = {"filter": status_filter} 298 | 299 | if branch: 300 | params["branch"] = branch 301 | 302 | slug = self.project_slug(username, project, vcs_type) 303 | endpoint = f"project/{slug}/latest/artifacts" 304 | resp = self._request(GET, endpoint, params=params) 305 | return resp 306 | 307 | def download_artifact(self, url, destdir=None, filename=None): 308 | """Download an artifact from a url. 309 | 310 | :param url: URL to the artifact. 311 | :param destdir: Destination directory. Defaults to None (current working directory). 312 | :param filename: File name. Defaults to the name of the artifact file. 313 | """ 314 | resp = self._download(url, destdir, filename) 315 | return resp 316 | 317 | def get_test_metadata(self, username, project, build_num, vcs_type=GITHUB): 318 | """Get test metadata for a build. 319 | 320 | :param username: Org or user name. 321 | :param project: Repo name. 322 | :param build_num: Build number. 323 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 324 | 325 | Endpoint: 326 | GET ``/project/:vcs-type/:username/:project/:build-num/tests`` 327 | """ 328 | slug = self.project_slug(username, project, vcs_type) 329 | endpoint = f"project/{slug}/{build_num}/tests" 330 | resp = self._request(GET, endpoint) 331 | return resp 332 | 333 | def retry_build(self, username, project, build_num, ssh=False, vcs_type=GITHUB): 334 | """Retry a build. 335 | 336 | :param username: Org or user name. 337 | :param project: Repo name. 338 | :param build_num: Build number. 339 | :param ssh: Retry a build with SSH enabled. Defaults to False. 340 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 341 | 342 | :type ssh: bool 343 | 344 | Endpoint: 345 | POST ``/project/:vcs-type/:username/:project/:build-num/{retry|ssh}`` 346 | """ 347 | action = "ssh" if ssh else "retry" 348 | slug = self.project_slug(username, project, vcs_type) 349 | endpoint = f"project/{slug}/{build_num}/{action}" 350 | resp = self._request(POST, endpoint) 351 | return resp 352 | 353 | def cancel_build(self, username, project, build_num, vcs_type=GITHUB): 354 | """Cancel a build. 355 | 356 | :param username: Org or user name. 357 | :param project: Repo name. 358 | :param build_num: Build number. 359 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 360 | 361 | Endpoint: 362 | POST ``/project/:vcs-type/:username/:project/:build-num/cancel`` 363 | """ 364 | slug = self.project_slug(username, project, vcs_type) 365 | endpoint = f"project/{slug}/{build_num}/cancel" 366 | resp = self._request(POST, endpoint) 367 | return resp 368 | 369 | def get_job_details(self, username, project, job_number, vcs_type=GITHUB): 370 | """Get job details. 371 | 372 | :param username: Org or user name. 373 | :param project: Repo name. 374 | :param job_number: Job number 375 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 376 | 377 | Endpoint: 378 | GET ``/project/:vcs-type/:username/:project/job/:job-number`` 379 | """ 380 | slug = self.project_slug(username, project, vcs_type) 381 | endpoint = f"project/{slug}/job/{job_number}" 382 | resp = self._request(GET, endpoint, api_version=API_VER_V2) 383 | return resp 384 | 385 | def cancel_job(self, username, project, job_number, vcs_type=GITHUB): 386 | """Cancel a job. 387 | 388 | :param username: Org or user name. 389 | :param project: Repo name. 390 | :param job_number: Job number. 391 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 392 | 393 | Endpoint: 394 | POST ``/project/:vcs-type/:username/:project/job/:job-number/cancel`` 395 | """ 396 | slug = self.project_slug(username, project, vcs_type) 397 | endpoint = f"project/{slug}/job/{job_number}/cancel" 398 | resp = self._request(POST, endpoint, api_version=API_VER_V2) 399 | return resp 400 | 401 | def get_project_pipelines(self, username, project, branch=None, mine=False, vcs_type=GITHUB, paginate=False, limit=None): 402 | """Get all pipelines configured for a project. 403 | 404 | :param username: Org or user name. 405 | :param project: Repo name. 406 | :param branch: Restricts returned pipelines to a single branch. Ignored if ``mine`` is True. 407 | :param mine: Restricts returned results to pipelines triggered by the current user. Defaults to False. 408 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 409 | :param paginate: If True, repeatedly requests more items from the endpoint until the limit has been reached (or until all results have been fetched). Defaults to False. 410 | :param limit: Maximum number of items to return. By default returns all the results from multiple calls to the endpoint, or all the results from a single call to the endpoint, depending on the value for ``paginate``. 411 | 412 | Endpoint: 413 | GET ``/project/:vcs-type/:username/:project/pipeline`` 414 | """ 415 | params = {"branch": branch} if branch else None 416 | 417 | slug = self.project_slug(username, project, vcs_type) 418 | endpoint = f"project/{slug}/pipeline" 419 | if mine: 420 | endpoint += "/mine" 421 | resp = self._request_get_items(endpoint, params=params, paginate=paginate, limit=limit) 422 | return resp 423 | 424 | def get_project_pipeline(self, username, project, pipeline_num, vcs_type=GITHUB): 425 | """Get full details of a given project pipeline by pipeline number. 426 | 427 | :param username: Org or user name. 428 | :param project: Repo name. 429 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 430 | :param pipeline_num: Pipeline number 431 | 432 | Endpoint: 433 | GET ``/project/:vcs-type/:username/:project/pipeline/:pipeline-number`` 434 | """ 435 | slug = self.project_slug(username, project, vcs_type) 436 | endpoint = f"project/{slug}/pipeline/{pipeline_num}" 437 | resp = self._request(GET, endpoint, api_version=API_VER_V2) 438 | return resp 439 | 440 | def continue_pipeline(self, continuation_key, config, params=None): 441 | """Continue a pipeline from the setup phase. 442 | 443 | :param continuation_key: Pipeline continuation key. 444 | :param config: Configuration string for the pipeline. 445 | :param params: Optional query parameters. 446 | 447 | :type params: dict 448 | 449 | Endpoint: 450 | POST ``/pipeline/continue`` 451 | """ 452 | data = {"continuation-key": continuation_key, "configuration": config} 453 | 454 | if params: 455 | data["parameters"] = params 456 | 457 | endpoint = "pipeline/continue" 458 | resp = self._request(POST, endpoint, data=data, api_version=API_VER_V2) 459 | return resp 460 | 461 | def get_pipelines(self, username, mine=False, vcs_type=GITHUB, paginate=False, limit=None): 462 | """Get all pipelines for the most recently built projects (max 250) you follow in an org. 463 | 464 | :param username: Org or user name. 465 | :param mine: Only include entries created by your user. Defaults to False. 466 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 467 | :param paginate: If True, repeatedly requests more items from the endpoint until the limit has been reached (or until all results have been fetched). Defaults to False. 468 | :param limit: Maximum number of items to return. By default returns all the results from multiple calls to the endpoint, or all the results from a single call to the endpoint, depending on the value for ``paginate``. 469 | 470 | Endpoint: 471 | GET ``/pipeline`` 472 | """ 473 | params = {"org-slug": self.owner_slug(username, vcs_type)} 474 | if mine: 475 | params["mine"] = True 476 | 477 | endpoint = "pipeline" 478 | resp = self._request_get_items(endpoint, params=params, paginate=paginate, limit=limit) 479 | return resp 480 | 481 | def get_pipeline(self, pipeline_id): 482 | """Get full details of a given pipeline. 483 | 484 | :param pipeline_id: Pipieline ID. 485 | 486 | Endpoint: 487 | GET ``/pipeline/:pipeline-id`` 488 | """ 489 | endpoint = f"pipeline/{pipeline_id}" 490 | resp = self._request(GET, endpoint, api_version=API_VER_V2) 491 | return resp 492 | 493 | def get_pipeline_config(self, pipeline_id): 494 | """Get the configuration of a given pipeline. 495 | 496 | :param pipeline_id: Pipieline ID. 497 | 498 | Endpoint: 499 | GET ``/pipeline/:pipeline-id/config`` 500 | """ 501 | endpoint = f"pipeline/{pipeline_id}/config" 502 | resp = self._request(GET, endpoint, api_version=API_VER_V2) 503 | return resp 504 | 505 | def get_pipeline_workflow(self, pipeline_id, paginate=False, limit=None): 506 | """Get the workflow of a given pipeline. 507 | 508 | :param pipeline_id: Pipieline ID. 509 | :param paginate: If True, repeatedly requests more items from the endpoint until the limit has been reached (or until all results have been fetched). Defaults to False. 510 | :param limit: Maximum number of items to return. By default returns all the results from multiple calls to the endpoint, or all the results from a single call to the endpoint, depending on the value for ``paginate``. 511 | 512 | Endpoint: 513 | GET ``/pipeline/:pipeline-id/workflow`` 514 | """ 515 | endpoint = f"pipeline/{pipeline_id}/workflow" 516 | resp = self._request_get_items(endpoint, paginate=paginate, limit=limit) 517 | return resp 518 | 519 | def get_workflow(self, workflow_id): 520 | """Get summary details of a given workflow. 521 | 522 | :param workflow_id: Workflow ID. 523 | 524 | Endpoint: 525 | GET ``/workflow/:workflow-id`` 526 | """ 527 | endpoint = f"workflow/{workflow_id}" 528 | resp = self._request(GET, endpoint, api_version=API_VER_V2) 529 | return resp 530 | 531 | def get_workflow_jobs(self, workflow_id, paginate=False, limit=None): 532 | """Get list of jobs of a given workflow. 533 | 534 | :param workflow_id: Workflow ID. 535 | :param paginate: If True, repeatedly requests more items from the endpoint until the limit has been reached (or until all results have been fetched). Defaults to False. 536 | :param limit: Maximum number of items to return. By default returns all the results from multiple calls to the endpoint, or all the results from a single call to the endpoint, depending on the value for ``paginate``. 537 | 538 | Endpoint: 539 | GET ``/workflow/:workflow-id/job`` 540 | """ 541 | endpoint = f"workflow/{workflow_id}/job" 542 | resp = self._request_get_items(endpoint, paginate=paginate, limit=limit) 543 | return resp 544 | 545 | def cancel_workflow(self, workflow_id): 546 | """Cancel a workflow. 547 | 548 | :param workflow_id: Workflow ID. 549 | 550 | Endpoint: 551 | POST ``/workflow/:workflow-id/cancel`` 552 | """ 553 | endpoint = f"workflow/{workflow_id}/cancel" 554 | resp = self._request(POST, endpoint, api_version=API_VER_V2) 555 | return resp 556 | 557 | def rerun_workflow(self, workflow_id, jobs=None, from_failed=False, sparse_tree=False): 558 | """Rerun a workflow. 559 | 560 | :param workflow_id: Workflow ID. 561 | :param jobs: List of job UUIDs to rerun. 562 | :param from_failed: Whether to rerun the workflow from the failed job. Mutually exclusive with the ``jobs`` parameter. 563 | :param sparse_tree: Completes rerun using sparse trees logic. Requires ``jobs`` parameter and so is mutually exclusive with the ``from_failed`` parameter. 564 | 565 | Endpoint: 566 | POST ``/workflow/:workflow-id/rerun`` 567 | """ 568 | data = {"from_failed": from_failed, "sparse_tree": sparse_tree} 569 | if jobs: 570 | data["jobs"] = jobs 571 | 572 | endpoint = f"workflow/{workflow_id}/rerun" 573 | resp = self._request(POST, endpoint, data=data, api_version=API_VER_V2) 574 | return resp 575 | 576 | def approve_job(self, workflow_id, approval_request_id): 577 | """Approve a pending approval job in a workflow. 578 | 579 | :param workflow_id: Workflow ID. 580 | :param approval_request_id: The ID of the job being approved. 581 | 582 | Endpoint: 583 | POST ``/workflow/:workflow-id/approve/:approval-request-id`` 584 | """ 585 | endpoint = f"workflow/{workflow_id}/approve/{approval_request_id}" 586 | resp = self._request(POST, endpoint, api_version=API_VER_V2) 587 | return resp 588 | 589 | def add_ssh_user(self, username, project, build_num, vcs_type=GITHUB): 590 | """Add a user to the build SSH permissions. 591 | 592 | :param username: Org or user name. 593 | :param project: Repo name. 594 | :param build_num: Build number. 595 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 596 | 597 | Endpoint: 598 | POST ``/project/:vcs-type/:username/:project/:build-num/ssh-users`` 599 | """ 600 | slug = self.project_slug(username, project, vcs_type) 601 | endpoint = f"project/{slug}/{build_num}/ssh-users" 602 | resp = self._request(POST, endpoint) 603 | return resp 604 | 605 | def trigger_build( 606 | self, 607 | username, 608 | project, 609 | branch="master", 610 | revision=None, 611 | tag=None, 612 | parallel=None, 613 | params=None, 614 | vcs_type=GITHUB, 615 | ): 616 | """Trigger a new build. 617 | 618 | .. note:: 619 | * ``tag`` and ``revision`` are mutually exclusive. 620 | * ``parallel`` is ignored for builds running on CircleCI 2.0 621 | 622 | :param username: Organization or user name. 623 | :param project: Repo name. 624 | :param branch: The branch to build. Defaults to master. 625 | :param revision: The specific git revision to build. 626 | Defaults to None and the head of the branch is used. 627 | Cannot be used with the ``tag`` parameter. 628 | :param tag: The git tag to build. 629 | Defaults to None. Cannot be used with the ``revision`` parameter. 630 | :param parallel: Number of containers to use to run the build. 631 | Defaults to None and the project default is used. 632 | :param params: Optional build parameters. 633 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 634 | 635 | :type params: dict 636 | :type parallel: int 637 | 638 | Endpoint: 639 | POST ``/project/:vcs-type/:username/:project/tree/:branch`` 640 | """ 641 | data = {"parallel": parallel, "revision": revision, "tag": tag} 642 | 643 | if params: 644 | data.update(params) 645 | 646 | slug = self.project_slug(username, project, vcs_type) 647 | endpoint = f"project/{slug}/tree/{branch}" 648 | resp = self._request(POST, endpoint, data=data) 649 | return resp 650 | 651 | def trigger_pipeline( 652 | self, 653 | username, 654 | project, 655 | branch=None, 656 | tag=None, 657 | params=None, 658 | vcs_type=GITHUB, 659 | ): 660 | """Trigger a new pipeline. 661 | 662 | .. note:: 663 | * ``tag`` and ``branch`` are mutually exclusive. 664 | 665 | :param username: Organization or user name. 666 | :param project: Repo name. 667 | :param branch: The branch to build. 668 | Defaults to None. Cannot be used with the ``tag`` parameter. 669 | :param tag: The git tag to build. 670 | Defaults to None. Cannot be used with the ``branch`` parameter. 671 | :param params: Optional build parameters. 672 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 673 | 674 | :type params: dict 675 | 676 | Endpoint: 677 | POST ``/project/:vcs-type/:username/:project/pipeline`` 678 | """ 679 | data = {} 680 | if branch: 681 | data["branch"] = branch 682 | elif tag: 683 | data["tag"] = tag 684 | 685 | if params: 686 | data["parameters"] = params 687 | 688 | slug = self.project_slug(username, project, vcs_type) 689 | endpoint = f"project/{slug}/pipeline" 690 | resp = self._request(POST, endpoint, data=data, api_version=API_VER_V2) 691 | return resp 692 | 693 | def add_ssh_key(self, username, project, ssh_key, vcs_type=GITHUB, hostname=None): 694 | """Create an SSH key. 695 | 696 | Used to access external systems that require SSH key-based authentication. 697 | 698 | .. note:: 699 | The ssh_key must be unencrypted. 700 | 701 | :param username: Org or user name. 702 | :param project: Repo name. 703 | :param branch: Branch name. Defaults to master. 704 | :param ssh_key: Private RSA key. 705 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 706 | :param hostname: Optional hostname. If set, the key will only work for this hostname. 707 | 708 | Endpoint: 709 | POST ``/project/:vcs-type/:username/:project/ssh-key`` 710 | """ 711 | params = {"hostname": hostname, "private_key": ssh_key} 712 | 713 | slug = self.project_slug(username, project, vcs_type) 714 | endpoint = f"project/{slug}/ssh-key" 715 | resp = self._request(POST, endpoint, data=params) 716 | return resp 717 | 718 | def list_checkout_keys(self, username, project, vcs_type=GITHUB): 719 | """Get list of checkout keys for a project. 720 | 721 | :param username: Org or user name. 722 | :param project: Repo name. 723 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 724 | 725 | Endpoint: 726 | GET ``/project/:vcs-type/:username/:project/checkout-key`` 727 | """ 728 | slug = self.project_slug(username, project, vcs_type) 729 | endpoint = f"project/{slug}/checkout-key" 730 | resp = self._request(GET, endpoint) 731 | return resp 732 | 733 | def create_checkout_key(self, username, project, key_type, vcs_type=GITHUB): 734 | """Create a new checkout key for a project. 735 | 736 | :param username: Org or user name. 737 | :param project: Repo name. 738 | :param key_type: Type of key to create. Valid values are: 739 | "deploy-key" or "github-user-key" 740 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 741 | 742 | Endpoint: 743 | POST ``/project/:vcs-type/:username/:project/checkout-key`` 744 | """ 745 | valid_types = ["deploy-key", "github-user-key"] 746 | if key_type not in valid_types: 747 | raise CircleciError(f"Invalid key type: {key_type}. Valid values are: {valid_types}") 748 | 749 | params = {"type": key_type} 750 | 751 | slug = self.project_slug(username, project, vcs_type) 752 | endpoint = f"project/{slug}/checkout-key" 753 | resp = self._request(POST, endpoint, data=params) 754 | return resp 755 | 756 | def get_checkout_key(self, username, project, fingerprint, vcs_type=GITHUB): 757 | """Get a checkout key. 758 | 759 | :param username: Org or user name. 760 | :param project: Repo name. 761 | :param fingerprint: The fingerprint of the checkout key. 762 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 763 | 764 | Endpoint: 765 | GET ``/project/:vcs-type/:username/:project/checkout-key/:fingerprint`` 766 | """ 767 | slug = self.project_slug(username, project, vcs_type) 768 | endpoint = f"project/{slug}/checkout-key/{fingerprint}" 769 | resp = self._request(GET, endpoint) 770 | return resp 771 | 772 | def delete_checkout_key(self, username, project, fingerprint, vcs_type=GITHUB): 773 | """Delete a checkout key. 774 | 775 | :param username: Org or user name. 776 | :param project: Repo name. 777 | :param fingerprint: The fingerprint of the checkout key. 778 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 779 | 780 | Endpoint: 781 | DELETE ``/project/:vcs-type/:username/:project/checkout-key/:fingerprint`` 782 | """ 783 | slug = self.project_slug(username, project, vcs_type) 784 | endpoint = f"project/{slug}/checkout-key/{fingerprint}" 785 | resp = self._request(DELETE, endpoint) 786 | return resp 787 | 788 | def list_envvars(self, username, project, vcs_type=GITHUB): 789 | """Get list of environment variables for a project. 790 | 791 | :param username: Org or user name. 792 | :param project: Repo name. 793 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 794 | 795 | Endpoint: 796 | GET ``/project/:vcs-type/:username/:project/envvar`` 797 | """ 798 | slug = self.project_slug(username, project, vcs_type) 799 | endpoint = f"project/{slug}/envvar" 800 | resp = self._request(GET, endpoint) 801 | return resp 802 | 803 | def add_envvar(self, username, project, name, value, vcs_type=GITHUB): 804 | """Add an environment variable to project. 805 | 806 | :param username: Org or user name. 807 | :param project: Repo name. 808 | :param name: Name of the environment variable. 809 | :param value: Value of the environment variable. 810 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 811 | 812 | Endpoint: 813 | POST ``/project/:vcs-type/:username/:project/envvar`` 814 | """ 815 | data = {"name": name, "value": value} 816 | 817 | slug = self.project_slug(username, project, vcs_type) 818 | endpoint = f"project/{slug}/envvar" 819 | resp = self._request(POST, endpoint, data=data) 820 | return resp 821 | 822 | def get_envvar(self, username, project, name, vcs_type=GITHUB): 823 | """Get the hidden value of an environment variable. 824 | 825 | :param username: Org or user name. 826 | :param project: Repo name. 827 | :param name: Name of the environment variable. 828 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 829 | 830 | Endpoint: 831 | GET ``/project/:vcs-type/:username/:project/envvar/:name`` 832 | """ 833 | slug = self.project_slug(username, project, vcs_type) 834 | endpoint = f"project/{slug}/envvar/{name}" 835 | resp = self._request(GET, endpoint) 836 | return resp 837 | 838 | def delete_envvar(self, username, project, name, vcs_type=GITHUB): 839 | """Delete an environment variable. 840 | 841 | :param username: Org or user name. 842 | :param project: Repo name. 843 | :param name: Name of the environment variable. 844 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 845 | 846 | Endpoint: 847 | DELETE ``/project/:vcs-type/:username/:project/envvar/:name`` 848 | """ 849 | slug = self.project_slug(username, project, vcs_type) 850 | endpoint = f"project/{slug}/envvar/{name}" 851 | resp = self._request(DELETE, endpoint) 852 | return resp 853 | 854 | def get_contexts(self, username=None, owner_id=None, owner_type=ORG, vcs_type=GITHUB, paginate=False, limit=None): 855 | """Get contexts for an organization. 856 | 857 | :param username: Org or user name. 858 | :param owner_id: UUID of owner (use either ``username`` or ``owner_id``). 859 | :param owner_type: Either ``organization`` or ``account``. Defaults to ``organization``. 860 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 861 | :param paginate: If True, repeatedly requests more items from the endpoint until the limit has been reached (or until all results have been fetched). Defaults to False.. 862 | :param limit: Maximum number of items to return. By default returns all the results from multiple calls to the endpoint, or all the results from a single call to the endpoint, depending on the value for ``paginate``. 863 | 864 | Endpoint: 865 | GET ``/context`` 866 | """ 867 | params = {"owner-type": owner_type} 868 | 869 | if username: 870 | params["owner-slug"] = self.owner_slug(username, vcs_type) 871 | elif owner_id: 872 | params["owner-id"] = owner_id 873 | 874 | endpoint = "context" 875 | resp = self._request_get_items(endpoint, params=params, paginate=paginate, limit=limit) 876 | return resp 877 | 878 | def get_context(self, context_id): 879 | """Get a context. 880 | 881 | :param context_id: UUID of context to get. 882 | 883 | Endpoint: 884 | GET ``/context/:context-id`` 885 | """ 886 | endpoint = f"context/{context_id}" 887 | resp = self._request(GET, endpoint, api_version=API_VER_V2) 888 | return resp 889 | 890 | def add_context(self, name, username=None, owner_id=None, owner_type=ORG, vcs_type=GITHUB): 891 | """Add a new context at org or account level. 892 | 893 | :param name: Context name to add. 894 | :param username: Org or user name. 895 | :param owner_id: UUID of owner (use either ``username`` or ``owner_id``). 896 | :param owner_type: Either ``organization`` or ``account``. Defaults to ``organization``. 897 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 898 | 899 | Endpoint: 900 | POST ``/context`` 901 | """ 902 | data = {"name": name, "owner": {"type": owner_type}} 903 | 904 | if username: 905 | data["owner"]["slug"] = self.owner_slug(username, vcs_type) 906 | elif owner_id: 907 | data["owner"]["id"] = owner_id 908 | 909 | endpoint = "context" 910 | resp = self._request(POST, endpoint, data=data, api_version=API_VER_V2) 911 | return resp 912 | 913 | def delete_context(self, context_id): 914 | """Delete a context. 915 | 916 | :param context_id: UUID of context to delete. 917 | 918 | Endpoint: 919 | DELETE ``/context/:context-id`` 920 | """ 921 | endpoint = f"context/{context_id}" 922 | resp = self._request(DELETE, endpoint, api_version=API_VER_V2) 923 | return resp 924 | 925 | def get_context_envvars(self, context_id, paginate=False, limit=None): 926 | """Get environment variables for a context. 927 | 928 | :param context_id: ID of context to retrieve environment variables from. 929 | :param paginate: If True, repeatedly requests more items from the endpoint until the limit has been reached (or until all results have been fetched). Defaults to False.. 930 | :param limit: Maximum number of items to return. By default returns all the results from multiple calls to the endpoint, or all the results from a single call to the endpoint, depending on the value for ``paginate``. 931 | 932 | Endpoint: 933 | GET ``/context/:context-id/environment-variable`` 934 | """ 935 | endpoint = f"context/{context_id}/environment-variable" 936 | resp = self._request_get_items(endpoint, paginate=paginate, limit=limit) 937 | return resp 938 | 939 | def add_context_envvar(self, context_id, name, value): 940 | """Add or update an environment variable to a context. 941 | 942 | :param context_id: ID of the context to add environment variable to. 943 | :param name: Name of the environment variable. 944 | :param value: Value of the environment variable. 945 | 946 | Endpoint: 947 | PUT ``/context/:context-id/environment-variable/:name`` 948 | """ 949 | data = {"value": value} 950 | endpoint = f"context/{context_id}/environment-variable/{name}" 951 | resp = self._request(PUT, endpoint, api_version=API_VER_V2, data=data) 952 | return resp 953 | 954 | def delete_context_envvar(self, context_id, name): 955 | """Delete an environment variable from a context. 956 | 957 | :param context_id: ID of the context to delete environment variable from. 958 | :param name: Name of the environment variable. 959 | 960 | Endpoint: 961 | DELETE ``/context/:context-id/environment-variable/:name`` 962 | """ 963 | endpoint = f"context/{context_id}/environment-variable/{name}" 964 | resp = self._request(DELETE, endpoint, api_version=API_VER_V2) 965 | return resp 966 | 967 | def get_project_settings(self, username, project, vcs_type=GITHUB): 968 | """Get project advanced settings. 969 | 970 | :param username: Org or user name. 971 | :param project: Repo name. 972 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 973 | 974 | Endpoint: 975 | GET ``/project/:vcs-type/:username/:project/settings`` 976 | """ 977 | slug = self.project_slug(username, project, vcs_type) 978 | endpoint = f"project/{slug}/settings" 979 | resp = self._request(GET, endpoint) 980 | return resp 981 | 982 | def update_project_settings(self, username, project, settings, vcs_type=GITHUB): 983 | """Update project advanced settings. 984 | 985 | :param username: Org or user name. 986 | :param project: Repo name. 987 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 988 | :param settings: Settings to update. 989 | Refer to mocks/get_project_settings_response.json for example settings. 990 | 991 | :type settings: dict 992 | 993 | Endpoint: 994 | PUT ``/project/:vcs-type/:username/:project/settings`` 995 | """ 996 | slug = self.project_slug(username, project, vcs_type) 997 | endpoint = f"project/{slug}/settings" 998 | resp = self._request(PUT, endpoint, data=settings) 999 | return resp 1000 | 1001 | def get_project_branches(self, username, project, workflow_name=None, vcs_type=GITHUB): 1002 | """Get all branches for a given project. The list will only contain branches currently available within Insights. 1003 | 1004 | :param username: Org or user name. 1005 | :param project: Repo name. 1006 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 1007 | 1008 | Endpoint: 1009 | GET ``/insights/:vcs-type/:username/:project/branches`` 1010 | """ 1011 | params = {"workflow-name": workflow_name} if workflow_name else None 1012 | 1013 | slug = self.project_slug(username, project, vcs_type) 1014 | endpoint = f"insights/{slug}/branches" 1015 | resp = self._request(GET, endpoint, params=params, api_version=API_VER_V2) 1016 | return resp 1017 | 1018 | def get_flaky_tests(self, username, project, vcs_type=GITHUB): 1019 | """Get a list of flaky tests for a given project. 1020 | 1021 | :param username: Org or user name. 1022 | :param project: Repo name. 1023 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 1024 | 1025 | Endpoint: 1026 | GET ``/insights/:vcs-type/:username/:project/flaky-tests`` 1027 | """ 1028 | slug = self.project_slug(username, project, vcs_type) 1029 | endpoint = f"insights/{slug}/flaky-tests" 1030 | resp = self._request(GET, endpoint, api_version=API_VER_V2) 1031 | return resp 1032 | 1033 | def get_project_workflows_metrics(self, username, project, params=None, vcs_type=GITHUB, paginate=False, limit=None): 1034 | """Get summary metrics for a project's workflows. 1035 | 1036 | :param username: Org or user name. 1037 | :param project: Repo name. 1038 | :param params: Optional query parameters. 1039 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 1040 | :param paginate: If True, repeatedly requests more items from the endpoint until the limit has been reached (or until all results have been fetched). Defaults to False. 1041 | :param limit: Maximum number of items to return. By default returns all the results from multiple calls to the endpoint, or all the results from a single call to the endpoint, depending on the value for ``paginate``. 1042 | 1043 | Endpoint: 1044 | GET ``/insights/:vcs-type/:username/:project/workflows`` 1045 | """ 1046 | slug = self.project_slug(username, project, vcs_type) 1047 | endpoint = f"insights/{slug}/workflows" 1048 | resp = self._request_get_items(endpoint, params=params, paginate=paginate, limit=limit) 1049 | return resp 1050 | 1051 | def get_project_workflow_metrics(self, username, project, workflow_name, params=None, vcs_type=GITHUB, paginate=False, limit=None): 1052 | """Get metrics of recent runs of a project workflow. 1053 | 1054 | :param username: Org or user name. 1055 | :param project: Repo name. 1056 | :param workflow_name: Workflow name 1057 | :param params: Optional query parameters. 1058 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 1059 | :param paginate: If True, repeatedly requests more items from the endpoint until the limit has been reached (or until all results have been fetched). Defaults to False. 1060 | :param limit: Maximum number of items to return. By default returns all the results from multiple calls to the endpoint, or all the results from a single call to the endpoint, depending on the value for ``paginate``. 1061 | 1062 | Endpoint: 1063 | GET ``/insights/:vcs-type/:username/:project/workflows/:workflow-name`` 1064 | """ 1065 | slug = self.project_slug(username, project, vcs_type) 1066 | endpoint = f"insights/{slug}/workflows/{workflow_name}" 1067 | resp = self._request_get_items(endpoint, params=params, paginate=paginate, limit=limit) 1068 | return resp 1069 | 1070 | def get_project_workflow_test_metrics(self, username, project, workflow_name, params=None, vcs_type=GITHUB): 1071 | """Get test metrics of recent runs of a project workflow. 1072 | 1073 | :param username: Org or user name. 1074 | :param project: Repo name. 1075 | :param workflow_name: Workflow name 1076 | :param params: Optional query parameters. 1077 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 1078 | 1079 | Endpoint: 1080 | GET ``/insights/:vcs-type/:username/:project/workflows/:workflow-name/test-metrics`` 1081 | """ 1082 | slug = self.project_slug(username, project, vcs_type) 1083 | endpoint = f"insights/{slug}/workflows/{workflow_name}/test-metrics" 1084 | resp = self._request(GET, endpoint, params=params, api_version=API_VER_V2) 1085 | return resp 1086 | 1087 | def get_project_workflow_jobs_metrics(self, username, project, workflow_name, params=None, vcs_type=GITHUB, paginate=False, limit=None): 1088 | """Get summary metrics for a project workflow's jobs. 1089 | 1090 | :param username: Org or user name. 1091 | :param project: Repo name. 1092 | :param workflow_name: Workflow name 1093 | :param params: Optional query parameters. 1094 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 1095 | :param paginate: If True, repeatedly requests more items from the endpoint until the limit has been reached (or until all results have been fetched). Defaults to False. 1096 | :param limit: Maximum number of items to return. By default returns all the results from multiple calls to the endpoint, or all the results from a single call to the endpoint, depending on the value for ``paginate``. 1097 | 1098 | Endpoint: 1099 | GET ``/insights/:vcs-type/:username/:project/workflows/:workflow-name/jobs`` 1100 | """ 1101 | slug = self.project_slug(username, project, vcs_type) 1102 | endpoint = f"insights/{slug}/workflows/{workflow_name}/jobs" 1103 | resp = self._request_get_items(endpoint, params=params, paginate=paginate, limit=limit) 1104 | return resp 1105 | 1106 | def get_project_workflow_job_metrics(self, username, project, workflow_name, job_name, params=None, vcs_type=GITHUB, paginate=False, limit=None): 1107 | """Get metrics of recent runs of a project workflow job. 1108 | 1109 | :param username: Org or user name. 1110 | :param project: Repo name. 1111 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 1112 | :param workflow_name: Workflow name 1113 | :param job_name: Job name 1114 | :param params: Optional query parameters. 1115 | :param paginate: If True, repeatedly requests more items from the endpoint until the limit has been reached (or until all results have been fetched). Defaults to False. 1116 | :param limit: Maximum number of items to return. By default returns all the results from multiple calls to the endpoint, or all the results from a single call to the endpoint, depending on the value for ``paginate``. 1117 | 1118 | Endpoint: 1119 | GET ``/insights/:vcs-type/:username/:project/workflows/:workflow-name/jobs/:job-name`` 1120 | """ 1121 | slug = self.project_slug(username, project, vcs_type) 1122 | endpoint = f"insights/{slug}/workflows/{workflow_name}/jobs/{job_name}" 1123 | resp = self._request_get_items(endpoint, params=params, paginate=paginate, limit=limit) 1124 | return resp 1125 | 1126 | def get_schedules(self, username, project, vcs_type=GITHUB): 1127 | """Get all schedules for a project. 1128 | 1129 | :param username: Org or user name. 1130 | :param project: Repo name. 1131 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 1132 | 1133 | Endpoint: 1134 | GET ``/project/:vcs-type/:username/:project/schedule`` 1135 | """ 1136 | slug = self.project_slug(username, project, vcs_type) 1137 | endpoint = f"project/{slug}/schedule" 1138 | resp = self._request(GET, endpoint, api_version=API_VER_V2) 1139 | return resp 1140 | 1141 | def get_schedule(self, schedule_id): 1142 | """Get a schedule. 1143 | 1144 | :param schedule_id: UUID of schedule to get. 1145 | 1146 | Endpoint: 1147 | GET ``/schedule/:schedule-id`` 1148 | """ 1149 | endpoint = f"schedule/{schedule_id}" 1150 | resp = self._request(GET, endpoint, api_version=API_VER_V2) 1151 | return resp 1152 | 1153 | def add_schedule(self, username, project, name, settings, vcs_type=GITHUB): 1154 | """Create a schedule. 1155 | 1156 | :param username: Org or user name. 1157 | :param project: Repo name. 1158 | :param name: Name of the schedule. 1159 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 1160 | :param settings: Schedule settings. 1161 | Refer to mocks/get_schedule_response.json for example settings. 1162 | 1163 | :type settings: dict 1164 | 1165 | Endpoint: 1166 | POST ``/project/:vcs-type/:username/:project/schedule`` 1167 | """ 1168 | data = {"name": name} 1169 | data.update(settings) 1170 | 1171 | slug = self.project_slug(username, project, vcs_type) 1172 | endpoint = f"project/{slug}/schedule" 1173 | resp = self._request(POST, endpoint, data=data, api_version=API_VER_V2) 1174 | return resp 1175 | 1176 | def update_schedule(self, schedule_id, settings): 1177 | """Update a schedule. 1178 | 1179 | :param schedule_id: UUID of schedule to update. 1180 | :param settings: Schedule settings. 1181 | Refer to mocks/get_schedule_response.json for example settings. 1182 | 1183 | :type settings: dict 1184 | 1185 | Endpoint: 1186 | PATCH ``/schedule/:schedule-id`` 1187 | """ 1188 | endpoint = f"schedule/{schedule_id}" 1189 | resp = self._request(PATCH, endpoint, data=settings, api_version=API_VER_V2) 1190 | return resp 1191 | 1192 | def delete_schedule(self, schedule_id): 1193 | """Delete a schedule. 1194 | 1195 | :param schedule_id: UUID of schedule to delete. 1196 | 1197 | Endpoint: 1198 | DELETE ``/schedule/:schedule-id`` 1199 | """ 1200 | endpoint = f"schedule/{schedule_id}" 1201 | resp = self._request(DELETE, endpoint, api_version=API_VER_V2) 1202 | return resp 1203 | 1204 | def project_slug(self, username, reponame, vcs_type=GITHUB): 1205 | """Get project slug. 1206 | 1207 | :param username: Org or user name. 1208 | :param reponame: Repo name. 1209 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 1210 | 1211 | :returns: string ``:vcs-type/:username/:reponame`` 1212 | """ 1213 | slug = f"{vcs_type}/{username}/{reponame}" 1214 | return slug 1215 | 1216 | def owner_slug(self, username, vcs_type=GITHUB): 1217 | """Get owner/org slug. 1218 | 1219 | :param username: Org or user name. 1220 | :param vcs_type: VCS type (github, bitbucket). Defaults to ``github``. 1221 | 1222 | :returns: string ``:vcs-type/:username"`` 1223 | """ 1224 | slug = f"{vcs_type}/{username}" 1225 | return slug 1226 | 1227 | def split_project_slug(self, slug): 1228 | """Split project slug into components. 1229 | 1230 | :param slug: Project slug. 1231 | 1232 | :returns: tuple ``(:vcs-type, :username, :reponame)`` 1233 | """ 1234 | parts = slug.split("/") 1235 | if len(parts) != 3: 1236 | raise CircleciError(f"Invalid project slug: '{slug}'") 1237 | return tuple(parts) 1238 | 1239 | def validate_api_version(self, api_version=None): 1240 | """Validate and normalize an API version value""" 1241 | api_version = API_VER_V1 if api_version is None else api_version 1242 | ver = str(api_version).lower() 1243 | if ver in [API_VER_V1, "v1", "1.1", "1", "1.0"]: 1244 | return API_VER_V1 1245 | if ver in [API_VER_V2, "2", "2.0"]: 1246 | return API_VER_V2 1247 | raise CircleciError(f"Invalid CircleCI API version: {api_version}. Valid values are: {API_VERSIONS}") 1248 | 1249 | def _request_session( 1250 | self, 1251 | retries=3, 1252 | backoff_factor=0.3, 1253 | status_forcelist=(408, 429, 500, 502, 503, 504, 520, 521, 522, 523, 524), 1254 | ): 1255 | """Get a session with Retry enabled. 1256 | 1257 | :param retries: Number of retries to allow. 1258 | :param backoff_factor: Backoff factor to apply between attempts. 1259 | :param status_forcelist: HTTP status codes to force a retry on. 1260 | 1261 | :returns: A requests.Session object. 1262 | """ 1263 | session = requests.Session() 1264 | retry = Retry( 1265 | total=retries, 1266 | backoff_factor=backoff_factor, 1267 | status_forcelist=status_forcelist, 1268 | allowed_methods=False, 1269 | raise_on_redirect=False, 1270 | raise_on_status=False, 1271 | respect_retry_after_header=False, 1272 | ) 1273 | adapter = HTTPAdapter(max_retries=retry) 1274 | session.mount("http://", adapter) 1275 | session.mount("https://", adapter) 1276 | return session 1277 | 1278 | def _request(self, verb, endpoint, data=None, params=None, api_version=None): 1279 | """Send an HTTP request. 1280 | 1281 | :param verb: HTTP method: DELETE, GET, PATCH, POST, PUT 1282 | :param endpoint: API endpoint to call. 1283 | :param data: Optional POST data. 1284 | :param params: Optional query parameters. 1285 | :param api_version: Optional API version. Defaults to v1.1 1286 | 1287 | :type data: dict 1288 | :type params: dict 1289 | 1290 | :raises requests.exceptions.HTTPError: When response code is not successful. 1291 | 1292 | :returns: A JSON object with the response from the API. 1293 | """ 1294 | headers = {"Accept": "application/json", "Circle-Token": self.token} 1295 | auth = HTTPBasicAuth(self.token, "") 1296 | resp = None 1297 | 1298 | api_version = self.validate_api_version(api_version) 1299 | request_url = f"{self.url}/{api_version}/{endpoint}" 1300 | 1301 | verb = verb.upper() 1302 | if verb == GET: 1303 | resp = self._session.get(request_url, params=params, auth=auth, headers=headers) 1304 | elif verb == POST: 1305 | resp = self._session.post(request_url, params=params, auth=auth, headers=headers, json=data) 1306 | elif verb == PUT: 1307 | resp = self._session.put(request_url, params=params, auth=auth, headers=headers, json=data) 1308 | elif verb == PATCH: 1309 | resp = self._session.patch(request_url, params=params, auth=auth, headers=headers, json=data) 1310 | elif verb == DELETE: 1311 | resp = self._session.delete(request_url, params=params, auth=auth, headers=headers) 1312 | else: 1313 | raise CircleciError(f"Invalid HTTP method: {verb}. Valid values are: {HTTP_METHODS}") 1314 | 1315 | self.last_response = resp 1316 | resp.raise_for_status() 1317 | return resp.json() 1318 | 1319 | def _request_get_items(self, endpoint, params=None, api_version=API_VER_V2, paginate=False, limit=None): 1320 | """Send one or more HTTP GET requests and optionally depaginate results, up to a limit. 1321 | 1322 | :param api_version: API version to use. Defaults to v2 1323 | :param endpoint: API endpoint to GET. 1324 | :param params: Optional query parameters. 1325 | :param paginate: If True, repeatedly requests more items from the endpoint until the limit has been reached (or until all results have been fetched). Defaults to False. 1326 | :param limit: Maximum number of items to return. By default returns all the results from multiple calls to the endpoint, or all the results from a single call to the endpoint, depending on the value for ``paginate``. 1327 | 1328 | :type params: dict 1329 | 1330 | :raises requests.exceptions.HTTPError: When response code is not successful. 1331 | 1332 | :returns: A list of items which are the combined results of the requests made. 1333 | """ 1334 | results = [] 1335 | page = 1 1336 | params = {} if params is None else params.copy() 1337 | 1338 | if api_version == API_VER_V1: 1339 | # Don't fetch more than limit, but limit to 100 per page max 1340 | params["per-page"] = limit if limit and limit < 100 else 100 1341 | 1342 | while True: 1343 | resp = self._request(GET, endpoint, params=params, api_version=api_version) 1344 | # Nested with v2 APIs; flat in v1 1345 | items = resp["items"] if "items" in resp else resp 1346 | results.extend(items) 1347 | 1348 | # Break on first iteration of the loop if we're not paginating, if 1349 | # we have an empty list from resp (v1), or if we've already hit our 1350 | # limit. 1351 | if not paginate or not resp or (limit and len(results) >= limit): 1352 | break 1353 | 1354 | page += 1 1355 | if api_version == API_VER_V2: 1356 | # Also break early if there's no next page (v2) 1357 | if not resp["next_page_token"]: 1358 | break 1359 | params["page-token"] = resp["next_page_token"] 1360 | else: 1361 | params["page"] = page 1362 | 1363 | return results[:limit] 1364 | 1365 | def _download(self, url, destdir=None, filename=None): 1366 | """Download artifact file by url. 1367 | 1368 | :param url: URL to the artifact. 1369 | :param destdir: Optional destination directory. Defaults to None (curent working directory). 1370 | :param filename: Optional file name. Defaults to the name of the artifact file. 1371 | """ 1372 | destdir = os.getcwd() if destdir is None else destdir 1373 | filename = url.split("/")[-1] if filename is None else filename 1374 | 1375 | headers = {CIRCLE_API_KEY_HEADER: self.token} 1376 | resp = self._session.get(url, headers=headers, stream=True) 1377 | 1378 | path = f"{destdir}/{filename}" 1379 | with open(path, "wb") as f: 1380 | for chunk in resp.iter_content(chunk_size=1024): 1381 | if chunk: 1382 | f.write(chunk) 1383 | 1384 | return path 1385 | --------------------------------------------------------------------------------