|
| 1 | +#!/usr/bin/env python |
| 2 | +# -*- coding: utf-8 -*- |
| 3 | +""" Python Wrapper for Bintray API |
| 4 | +
|
| 5 | + https://bintray.com/docs/api |
| 6 | +""" |
| 7 | +import os |
| 8 | +import logging |
| 9 | +import requests |
| 10 | +from requests.auth import HTTPBasicAuth |
| 11 | + |
| 12 | + |
| 13 | +class Bintray(object): |
| 14 | + """ Python Wrapper for Bintray API |
| 15 | +
|
| 16 | + """ |
| 17 | + |
| 18 | + # Bintray API URL |
| 19 | + BINTRAY_URL = "https://api.bintray.com" |
| 20 | + |
| 21 | + def __init__(self, username=None, api_key=None): |
| 22 | + """ Initialize arguments for login |
| 23 | +
|
| 24 | + :param username: Bintray username |
| 25 | + :param api_key: Bintray API Key |
| 26 | + """ |
| 27 | + self._username = username or os.getenv("BINTRAY_USERNAME") |
| 28 | + self._password = api_key or os.getenv("BINTRAY_API_KEY") |
| 29 | + |
| 30 | + self._logger = logging.getLogger(__file__) |
| 31 | + self._logger.setLevel(logging.INFO) |
| 32 | + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') |
| 33 | + ch = logging.StreamHandler() |
| 34 | + ch.setLevel(logging.INFO) |
| 35 | + ch.setFormatter(formatter) |
| 36 | + self._logger.addHandler(ch) |
| 37 | + |
| 38 | + def _get_authentication(self): |
| 39 | + """ Retrieve Basic HTTP Authentication based on username and API key |
| 40 | +
|
| 41 | + :return: Basic Authentication handler |
| 42 | + """ |
| 43 | + if not self._username or not self._password: |
| 44 | + return None |
| 45 | + return HTTPBasicAuth(self._username, self._password) |
| 46 | + |
| 47 | + def _add_status_code(self, response): |
| 48 | + """ Update JSON result with error and status code |
| 49 | +
|
| 50 | + :param response: Requests response |
| 51 | + :return: Response JSON |
| 52 | + """ |
| 53 | + json_data = response.json() |
| 54 | + if isinstance(json_data, list): |
| 55 | + json_data.append({"statusCode": response.status_code, "error": not response.ok}) |
| 56 | + else: |
| 57 | + json_data.update({"statusCode": response.status_code, "error": not response.ok}) |
| 58 | + return json_data |
| 59 | + |
| 60 | + |
| 61 | + def _bool_to_number(self, value): |
| 62 | + """ Convert boolean result into numeric string |
| 63 | +
|
| 64 | + :param value: Any boolean value |
| 65 | + :return: "1" when True. Otherwise, "0" |
| 66 | + """ |
| 67 | + return "1" if value else "0" |
| 68 | + |
| 69 | + def _raise_error(self, message, response): |
| 70 | + try: |
| 71 | + response.raise_for_status() |
| 72 | + except Exception as error: |
| 73 | + raise Exception("{} ({}): {}".format(message, response.status_code, str(error))) |
| 74 | + |
| 75 | + # Files |
| 76 | + |
| 77 | + def get_package_files(self, subject, repo, package, include_unpublished=False): |
| 78 | + """ Get all files in a given package. |
| 79 | +
|
| 80 | + When called by a user with publishing rights on the package, |
| 81 | + includes unpublished files in the list. By default only published files are shown. |
| 82 | + :param subject: username or organization |
| 83 | + :param repo: repository name |
| 84 | + :param package: package name |
| 85 | + :param include_unpublished: Show not published files |
| 86 | + :return: List with all files |
| 87 | + """ |
| 88 | + parameters = {"include_unpublished": self._bool_to_number(include_unpublished)} |
| 89 | + url = "{}/packages/{}/{}/{}/files?include_unpublished={}".format(Bintray.BINTRAY_URL, |
| 90 | + subject, |
| 91 | + repo, |
| 92 | + package, |
| 93 | + include_unpublished) |
| 94 | + response = requests.get(url, auth=self._get_authentication(), params=parameters) |
| 95 | + if not response.ok: |
| 96 | + self._raise_error("Could not list package files", response) |
| 97 | + return self._add_status_code(response) |
| 98 | + |
| 99 | + # Content Uploading & Publishing |
| 100 | + |
| 101 | + def upload_content(self, subject, repo, package, version, remote_file_path, local_file_path, |
| 102 | + publish=True, override=False, explode=False): |
| 103 | + """ |
| 104 | + Upload content to the specified repository path, with package and version information (both required). |
| 105 | +
|
| 106 | + :param subject: username or organization |
| 107 | + :param repo: repository name |
| 108 | + :param package: package name |
| 109 | + :param version: package version |
| 110 | + :param remote_file_path: file name to be used on Bintray |
| 111 | + :param local_file_path: file path to be uploaded |
| 112 | + :param publish: publish after uploading |
| 113 | + :param override: override remote file |
| 114 | + :param explode: explode remote file |
| 115 | + :return: |
| 116 | + """ |
| 117 | + url = "{}/content/{}/{}/{}/{}/{}".format(Bintray.BINTRAY_URL, subject, repo, package, |
| 118 | + version, remote_file_path) |
| 119 | + parameters = {"publish": self._bool_to_number(publish), |
| 120 | + "override": self._bool_to_number(override), |
| 121 | + "explode": self._bool_to_number(explode)} |
| 122 | + |
| 123 | + with open(local_file_path, 'rb') as file_content: |
| 124 | + response = requests.put(url, auth=self._get_authentication(), params=parameters, |
| 125 | + data=file_content) |
| 126 | + if response.status_code != 201: |
| 127 | + self._raise_error("Could not upload", response) |
| 128 | + self._logger.info("Upload successfully: {}".format(url)) |
| 129 | + return self._add_status_code(response) |
| 130 | + |
| 131 | + # Content Downloading |
| 132 | + |
| 133 | + def download_content(self, subject, repo, remote_file_path, local_file_path): |
| 134 | + """ Download content from the specified repository path. |
| 135 | +
|
| 136 | + :param subject: username or organization |
| 137 | + :param repo: repository name |
| 138 | + :param remote_file_path: file name to be downloaded from Bintray |
| 139 | + :param local_file_path: file name to be stored in local storage |
| 140 | + """ |
| 141 | + download_base_url = "https://dl.bintray.com" |
| 142 | + url = "{}/{}/{}/{}".format(download_base_url, subject, repo, remote_file_path) |
| 143 | + response = requests.get(url, auth=self._get_authentication()) |
| 144 | + if not response.ok: |
| 145 | + self._raise_error("Could not download file content", response) |
| 146 | + with open(local_file_path, 'wb') as local_fd: |
| 147 | + local_fd.write(response.content) |
| 148 | + self._logger.info("Download successfully: {}".format(url)) |
| 149 | + return self._add_status_code(response) |
0 commit comments