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