buildkite/download-artifact/download_artifacts.py (68 lines of code) (raw):
from typing import Optional
from dataclasses import dataclass
import globber
import requests
import os
BUILDKITE_API_ACCESS_TOKEN = os.environ["BUILDKITE_API_ACCESS_TOKEN"]
HEADERS = {'Authorization': f'Bearer {BUILDKITE_API_ACCESS_TOKEN}'}
@dataclass
class ListArtifactsRequestURLBuilder:
org: str
pipeline: str
build_number: str
per_page = 100
def get_url(self) -> str:
return (
'https://api.buildkite.com/v2'
f'/organizations/{self.org}'
f'/pipelines/{self.pipeline}'
f'/builds/{self.build_number}'
'/artifacts'
f'?per_page={self.per_page}'
)
def find_all_matching_artifacts(artifacts: list, pattern: str) -> list:
"""
Finds all artifacts matching the given pattern.
:param artifacts: https://buildkite.com/docs/apis/rest-api/artifacts#list-artifacts-for-a-build
:param pattern: glob path
:return: list of artifacts
"""
return [artifact for artifact in artifacts if globber.match(pattern, artifact["path"])]
def get_next_artifacts_url(artifacts_response) -> Optional[str]:
"""
Gets the next artifacts list URL from an artifacts list response.
https://buildkite.com/docs/apis/rest-api/artifacts#list-artifacts-for-a-build
:param artifacts_response: Paginated list response (https://buildkite.com/docs/apis/rest-api#pagination)
:return: The next artifacts url or None
"""
if "next" in artifacts_response.links:
return artifacts_response.links["next"]["url"]
else:
return None
def download_artifacts(artifacts: list) -> None:
"""
Downloads artifacts to the current directory and maintains the folder structure.
:param artifacts: https://buildkite.com/docs/apis/rest-api/artifacts#list-artifacts-for-a-build
"""
for artifact in artifacts:
# https://buildkite.com/docs/apis/rest-api/artifacts#download-an-artifact
download_response = requests.get(
artifact["download_url"],
headers=HEADERS,
allow_redirects=True
)
os.makedirs(artifact["dirname"], exist_ok=True)
with open(artifact["path"], 'wb') as f:
f.write(download_response.content)
def get_artifacts_to_download(pattern: str, request_url: str, artifacts: list = None) -> list:
"""
Recursively gets all artifacts match the given pattern from a paginated artifacts list request.
See https://buildkite.com/docs/apis/rest-api/artifacts#list-artifacts-for-a-build
:param pattern: glob path
:param request_url: artifacts list request URL
:param artifacts: buildkite artifacts
:return:
"""
if artifacts is None:
artifacts = []
response = requests.get(
request_url,
headers=HEADERS
)
matching_artifacts = find_all_matching_artifacts(
response.json(),
pattern
)
next_url = get_next_artifacts_url(response)
combined_artifacts = artifacts + matching_artifacts
if next_url is None:
return combined_artifacts
else:
return get_artifacts_to_download(pattern, next_url, combined_artifacts)
def run() -> None:
org = os.environ["ORG"]
pipeline = os.environ["PIPELINE"]
build_number = os.environ["BUILD_NUMBER"]
artifact_path = os.environ["ARTIFACT_PATH"]
request = ListArtifactsRequestURLBuilder(org, pipeline, build_number)
artifacts_to_download = get_artifacts_to_download(
artifact_path,
request.get_url()
)
download_artifacts(artifacts_to_download)
run()