17db96d56Sopenharmony_ci""" 27db96d56Sopenharmony_cidistutils.command.upload 37db96d56Sopenharmony_ci 47db96d56Sopenharmony_ciImplements the Distutils 'upload' subcommand (upload package to a package 57db96d56Sopenharmony_ciindex). 67db96d56Sopenharmony_ci""" 77db96d56Sopenharmony_ci 87db96d56Sopenharmony_ciimport os 97db96d56Sopenharmony_ciimport io 107db96d56Sopenharmony_ciimport hashlib 117db96d56Sopenharmony_cifrom base64 import standard_b64encode 127db96d56Sopenharmony_cifrom urllib.error import HTTPError 137db96d56Sopenharmony_cifrom urllib.request import urlopen, Request 147db96d56Sopenharmony_cifrom urllib.parse import urlparse 157db96d56Sopenharmony_cifrom distutils.errors import DistutilsError, DistutilsOptionError 167db96d56Sopenharmony_cifrom distutils.core import PyPIRCCommand 177db96d56Sopenharmony_cifrom distutils.spawn import spawn 187db96d56Sopenharmony_cifrom distutils import log 197db96d56Sopenharmony_ci 207db96d56Sopenharmony_ci 217db96d56Sopenharmony_ci# PyPI Warehouse supports MD5, SHA256, and Blake2 (blake2-256) 227db96d56Sopenharmony_ci# https://bugs.python.org/issue40698 237db96d56Sopenharmony_ci_FILE_CONTENT_DIGESTS = { 247db96d56Sopenharmony_ci "md5_digest": getattr(hashlib, "md5", None), 257db96d56Sopenharmony_ci "sha256_digest": getattr(hashlib, "sha256", None), 267db96d56Sopenharmony_ci "blake2_256_digest": getattr(hashlib, "blake2b", None), 277db96d56Sopenharmony_ci} 287db96d56Sopenharmony_ci 297db96d56Sopenharmony_ci 307db96d56Sopenharmony_ciclass upload(PyPIRCCommand): 317db96d56Sopenharmony_ci 327db96d56Sopenharmony_ci description = "upload binary package to PyPI" 337db96d56Sopenharmony_ci 347db96d56Sopenharmony_ci user_options = PyPIRCCommand.user_options + [ 357db96d56Sopenharmony_ci ('sign', 's', 367db96d56Sopenharmony_ci 'sign files to upload using gpg'), 377db96d56Sopenharmony_ci ('identity=', 'i', 'GPG identity used to sign files'), 387db96d56Sopenharmony_ci ] 397db96d56Sopenharmony_ci 407db96d56Sopenharmony_ci boolean_options = PyPIRCCommand.boolean_options + ['sign'] 417db96d56Sopenharmony_ci 427db96d56Sopenharmony_ci def initialize_options(self): 437db96d56Sopenharmony_ci PyPIRCCommand.initialize_options(self) 447db96d56Sopenharmony_ci self.username = '' 457db96d56Sopenharmony_ci self.password = '' 467db96d56Sopenharmony_ci self.show_response = 0 477db96d56Sopenharmony_ci self.sign = False 487db96d56Sopenharmony_ci self.identity = None 497db96d56Sopenharmony_ci 507db96d56Sopenharmony_ci def finalize_options(self): 517db96d56Sopenharmony_ci PyPIRCCommand.finalize_options(self) 527db96d56Sopenharmony_ci if self.identity and not self.sign: 537db96d56Sopenharmony_ci raise DistutilsOptionError( 547db96d56Sopenharmony_ci "Must use --sign for --identity to have meaning" 557db96d56Sopenharmony_ci ) 567db96d56Sopenharmony_ci config = self._read_pypirc() 577db96d56Sopenharmony_ci if config != {}: 587db96d56Sopenharmony_ci self.username = config['username'] 597db96d56Sopenharmony_ci self.password = config['password'] 607db96d56Sopenharmony_ci self.repository = config['repository'] 617db96d56Sopenharmony_ci self.realm = config['realm'] 627db96d56Sopenharmony_ci 637db96d56Sopenharmony_ci # getting the password from the distribution 647db96d56Sopenharmony_ci # if previously set by the register command 657db96d56Sopenharmony_ci if not self.password and self.distribution.password: 667db96d56Sopenharmony_ci self.password = self.distribution.password 677db96d56Sopenharmony_ci 687db96d56Sopenharmony_ci def run(self): 697db96d56Sopenharmony_ci if not self.distribution.dist_files: 707db96d56Sopenharmony_ci msg = ("Must create and upload files in one command " 717db96d56Sopenharmony_ci "(e.g. setup.py sdist upload)") 727db96d56Sopenharmony_ci raise DistutilsOptionError(msg) 737db96d56Sopenharmony_ci for command, pyversion, filename in self.distribution.dist_files: 747db96d56Sopenharmony_ci self.upload_file(command, pyversion, filename) 757db96d56Sopenharmony_ci 767db96d56Sopenharmony_ci def upload_file(self, command, pyversion, filename): 777db96d56Sopenharmony_ci # Makes sure the repository URL is compliant 787db96d56Sopenharmony_ci schema, netloc, url, params, query, fragments = \ 797db96d56Sopenharmony_ci urlparse(self.repository) 807db96d56Sopenharmony_ci if params or query or fragments: 817db96d56Sopenharmony_ci raise AssertionError("Incompatible url %s" % self.repository) 827db96d56Sopenharmony_ci 837db96d56Sopenharmony_ci if schema not in ('http', 'https'): 847db96d56Sopenharmony_ci raise AssertionError("unsupported schema " + schema) 857db96d56Sopenharmony_ci 867db96d56Sopenharmony_ci # Sign if requested 877db96d56Sopenharmony_ci if self.sign: 887db96d56Sopenharmony_ci gpg_args = ["gpg", "--detach-sign", "-a", filename] 897db96d56Sopenharmony_ci if self.identity: 907db96d56Sopenharmony_ci gpg_args[2:2] = ["--local-user", self.identity] 917db96d56Sopenharmony_ci spawn(gpg_args, 927db96d56Sopenharmony_ci dry_run=self.dry_run) 937db96d56Sopenharmony_ci 947db96d56Sopenharmony_ci # Fill in the data - send all the meta-data in case we need to 957db96d56Sopenharmony_ci # register a new release 967db96d56Sopenharmony_ci f = open(filename,'rb') 977db96d56Sopenharmony_ci try: 987db96d56Sopenharmony_ci content = f.read() 997db96d56Sopenharmony_ci finally: 1007db96d56Sopenharmony_ci f.close() 1017db96d56Sopenharmony_ci 1027db96d56Sopenharmony_ci meta = self.distribution.metadata 1037db96d56Sopenharmony_ci data = { 1047db96d56Sopenharmony_ci # action 1057db96d56Sopenharmony_ci ':action': 'file_upload', 1067db96d56Sopenharmony_ci 'protocol_version': '1', 1077db96d56Sopenharmony_ci 1087db96d56Sopenharmony_ci # identify release 1097db96d56Sopenharmony_ci 'name': meta.get_name(), 1107db96d56Sopenharmony_ci 'version': meta.get_version(), 1117db96d56Sopenharmony_ci 1127db96d56Sopenharmony_ci # file content 1137db96d56Sopenharmony_ci 'content': (os.path.basename(filename),content), 1147db96d56Sopenharmony_ci 'filetype': command, 1157db96d56Sopenharmony_ci 'pyversion': pyversion, 1167db96d56Sopenharmony_ci 1177db96d56Sopenharmony_ci # additional meta-data 1187db96d56Sopenharmony_ci 'metadata_version': '1.0', 1197db96d56Sopenharmony_ci 'summary': meta.get_description(), 1207db96d56Sopenharmony_ci 'home_page': meta.get_url(), 1217db96d56Sopenharmony_ci 'author': meta.get_contact(), 1227db96d56Sopenharmony_ci 'author_email': meta.get_contact_email(), 1237db96d56Sopenharmony_ci 'license': meta.get_licence(), 1247db96d56Sopenharmony_ci 'description': meta.get_long_description(), 1257db96d56Sopenharmony_ci 'keywords': meta.get_keywords(), 1267db96d56Sopenharmony_ci 'platform': meta.get_platforms(), 1277db96d56Sopenharmony_ci 'classifiers': meta.get_classifiers(), 1287db96d56Sopenharmony_ci 'download_url': meta.get_download_url(), 1297db96d56Sopenharmony_ci # PEP 314 1307db96d56Sopenharmony_ci 'provides': meta.get_provides(), 1317db96d56Sopenharmony_ci 'requires': meta.get_requires(), 1327db96d56Sopenharmony_ci 'obsoletes': meta.get_obsoletes(), 1337db96d56Sopenharmony_ci } 1347db96d56Sopenharmony_ci 1357db96d56Sopenharmony_ci data['comment'] = '' 1367db96d56Sopenharmony_ci 1377db96d56Sopenharmony_ci # file content digests 1387db96d56Sopenharmony_ci for digest_name, digest_cons in _FILE_CONTENT_DIGESTS.items(): 1397db96d56Sopenharmony_ci if digest_cons is None: 1407db96d56Sopenharmony_ci continue 1417db96d56Sopenharmony_ci try: 1427db96d56Sopenharmony_ci data[digest_name] = digest_cons(content).hexdigest() 1437db96d56Sopenharmony_ci except ValueError: 1447db96d56Sopenharmony_ci # hash digest not available or blocked by security policy 1457db96d56Sopenharmony_ci pass 1467db96d56Sopenharmony_ci 1477db96d56Sopenharmony_ci if self.sign: 1487db96d56Sopenharmony_ci with open(filename + ".asc", "rb") as f: 1497db96d56Sopenharmony_ci data['gpg_signature'] = (os.path.basename(filename) + ".asc", 1507db96d56Sopenharmony_ci f.read()) 1517db96d56Sopenharmony_ci 1527db96d56Sopenharmony_ci # set up the authentication 1537db96d56Sopenharmony_ci user_pass = (self.username + ":" + self.password).encode('ascii') 1547db96d56Sopenharmony_ci # The exact encoding of the authentication string is debated. 1557db96d56Sopenharmony_ci # Anyway PyPI only accepts ascii for both username or password. 1567db96d56Sopenharmony_ci auth = "Basic " + standard_b64encode(user_pass).decode('ascii') 1577db96d56Sopenharmony_ci 1587db96d56Sopenharmony_ci # Build up the MIME payload for the POST data 1597db96d56Sopenharmony_ci boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254' 1607db96d56Sopenharmony_ci sep_boundary = b'\r\n--' + boundary.encode('ascii') 1617db96d56Sopenharmony_ci end_boundary = sep_boundary + b'--\r\n' 1627db96d56Sopenharmony_ci body = io.BytesIO() 1637db96d56Sopenharmony_ci for key, value in data.items(): 1647db96d56Sopenharmony_ci title = '\r\nContent-Disposition: form-data; name="%s"' % key 1657db96d56Sopenharmony_ci # handle multiple entries for the same name 1667db96d56Sopenharmony_ci if not isinstance(value, list): 1677db96d56Sopenharmony_ci value = [value] 1687db96d56Sopenharmony_ci for value in value: 1697db96d56Sopenharmony_ci if type(value) is tuple: 1707db96d56Sopenharmony_ci title += '; filename="%s"' % value[0] 1717db96d56Sopenharmony_ci value = value[1] 1727db96d56Sopenharmony_ci else: 1737db96d56Sopenharmony_ci value = str(value).encode('utf-8') 1747db96d56Sopenharmony_ci body.write(sep_boundary) 1757db96d56Sopenharmony_ci body.write(title.encode('utf-8')) 1767db96d56Sopenharmony_ci body.write(b"\r\n\r\n") 1777db96d56Sopenharmony_ci body.write(value) 1787db96d56Sopenharmony_ci body.write(end_boundary) 1797db96d56Sopenharmony_ci body = body.getvalue() 1807db96d56Sopenharmony_ci 1817db96d56Sopenharmony_ci msg = "Submitting %s to %s" % (filename, self.repository) 1827db96d56Sopenharmony_ci self.announce(msg, log.INFO) 1837db96d56Sopenharmony_ci 1847db96d56Sopenharmony_ci # build the Request 1857db96d56Sopenharmony_ci headers = { 1867db96d56Sopenharmony_ci 'Content-type': 'multipart/form-data; boundary=%s' % boundary, 1877db96d56Sopenharmony_ci 'Content-length': str(len(body)), 1887db96d56Sopenharmony_ci 'Authorization': auth, 1897db96d56Sopenharmony_ci } 1907db96d56Sopenharmony_ci 1917db96d56Sopenharmony_ci request = Request(self.repository, data=body, 1927db96d56Sopenharmony_ci headers=headers) 1937db96d56Sopenharmony_ci # send the data 1947db96d56Sopenharmony_ci try: 1957db96d56Sopenharmony_ci result = urlopen(request) 1967db96d56Sopenharmony_ci status = result.getcode() 1977db96d56Sopenharmony_ci reason = result.msg 1987db96d56Sopenharmony_ci except HTTPError as e: 1997db96d56Sopenharmony_ci status = e.code 2007db96d56Sopenharmony_ci reason = e.msg 2017db96d56Sopenharmony_ci except OSError as e: 2027db96d56Sopenharmony_ci self.announce(str(e), log.ERROR) 2037db96d56Sopenharmony_ci raise 2047db96d56Sopenharmony_ci 2057db96d56Sopenharmony_ci if status == 200: 2067db96d56Sopenharmony_ci self.announce('Server response (%s): %s' % (status, reason), 2077db96d56Sopenharmony_ci log.INFO) 2087db96d56Sopenharmony_ci if self.show_response: 2097db96d56Sopenharmony_ci text = self._read_pypi_response(result) 2107db96d56Sopenharmony_ci msg = '\n'.join(('-' * 75, text, '-' * 75)) 2117db96d56Sopenharmony_ci self.announce(msg, log.INFO) 2127db96d56Sopenharmony_ci else: 2137db96d56Sopenharmony_ci msg = 'Upload failed (%s): %s' % (status, reason) 2147db96d56Sopenharmony_ci self.announce(msg, log.ERROR) 2157db96d56Sopenharmony_ci raise DistutilsError(msg) 216