python (3.11.7)

(root)/
lib/
python3.11/
distutils/
command/
upload.py
       1  """
       2  distutils.command.upload
       3  
       4  Implements the Distutils 'upload' subcommand (upload package to a package
       5  index).
       6  """
       7  
       8  import os
       9  import io
      10  import hashlib
      11  from base64 import standard_b64encode
      12  from urllib.error import HTTPError
      13  from urllib.request import urlopen, Request
      14  from urllib.parse import urlparse
      15  from distutils.errors import DistutilsError, DistutilsOptionError
      16  from distutils.core import PyPIRCCommand
      17  from distutils.spawn import spawn
      18  from distutils import log
      19  
      20  
      21  # PyPI Warehouse supports MD5, SHA256, and Blake2 (blake2-256)
      22  # https://bugs.python.org/issue40698
      23  _FILE_CONTENT_DIGESTS = {
      24      "md5_digest": getattr(hashlib, "md5", None),
      25      "sha256_digest": getattr(hashlib, "sha256", None),
      26      "blake2_256_digest": getattr(hashlib, "blake2b", None),
      27  }
      28  
      29  
      30  class ESC[4;38;5;81mupload(ESC[4;38;5;149mPyPIRCCommand):
      31  
      32      description = "upload binary package to PyPI"
      33  
      34      user_options = PyPIRCCommand.user_options + [
      35          ('sign', 's',
      36           'sign files to upload using gpg'),
      37          ('identity=', 'i', 'GPG identity used to sign files'),
      38          ]
      39  
      40      boolean_options = PyPIRCCommand.boolean_options + ['sign']
      41  
      42      def initialize_options(self):
      43          PyPIRCCommand.initialize_options(self)
      44          self.username = ''
      45          self.password = ''
      46          self.show_response = 0
      47          self.sign = False
      48          self.identity = None
      49  
      50      def finalize_options(self):
      51          PyPIRCCommand.finalize_options(self)
      52          if self.identity and not self.sign:
      53              raise DistutilsOptionError(
      54                  "Must use --sign for --identity to have meaning"
      55              )
      56          config = self._read_pypirc()
      57          if config != {}:
      58              self.username = config['username']
      59              self.password = config['password']
      60              self.repository = config['repository']
      61              self.realm = config['realm']
      62  
      63          # getting the password from the distribution
      64          # if previously set by the register command
      65          if not self.password and self.distribution.password:
      66              self.password = self.distribution.password
      67  
      68      def run(self):
      69          if not self.distribution.dist_files:
      70              msg = ("Must create and upload files in one command "
      71                     "(e.g. setup.py sdist upload)")
      72              raise DistutilsOptionError(msg)
      73          for command, pyversion, filename in self.distribution.dist_files:
      74              self.upload_file(command, pyversion, filename)
      75  
      76      def upload_file(self, command, pyversion, filename):
      77          # Makes sure the repository URL is compliant
      78          schema, netloc, url, params, query, fragments = \
      79              urlparse(self.repository)
      80          if params or query or fragments:
      81              raise AssertionError("Incompatible url %s" % self.repository)
      82  
      83          if schema not in ('http', 'https'):
      84              raise AssertionError("unsupported schema " + schema)
      85  
      86          # Sign if requested
      87          if self.sign:
      88              gpg_args = ["gpg", "--detach-sign", "-a", filename]
      89              if self.identity:
      90                  gpg_args[2:2] = ["--local-user", self.identity]
      91              spawn(gpg_args,
      92                    dry_run=self.dry_run)
      93  
      94          # Fill in the data - send all the meta-data in case we need to
      95          # register a new release
      96          f = open(filename,'rb')
      97          try:
      98              content = f.read()
      99          finally:
     100              f.close()
     101  
     102          meta = self.distribution.metadata
     103          data = {
     104              # action
     105              ':action': 'file_upload',
     106              'protocol_version': '1',
     107  
     108              # identify release
     109              'name': meta.get_name(),
     110              'version': meta.get_version(),
     111  
     112              # file content
     113              'content': (os.path.basename(filename),content),
     114              'filetype': command,
     115              'pyversion': pyversion,
     116  
     117              # additional meta-data
     118              'metadata_version': '1.0',
     119              'summary': meta.get_description(),
     120              'home_page': meta.get_url(),
     121              'author': meta.get_contact(),
     122              'author_email': meta.get_contact_email(),
     123              'license': meta.get_licence(),
     124              'description': meta.get_long_description(),
     125              'keywords': meta.get_keywords(),
     126              'platform': meta.get_platforms(),
     127              'classifiers': meta.get_classifiers(),
     128              'download_url': meta.get_download_url(),
     129              # PEP 314
     130              'provides': meta.get_provides(),
     131              'requires': meta.get_requires(),
     132              'obsoletes': meta.get_obsoletes(),
     133              }
     134  
     135          data['comment'] = ''
     136  
     137          # file content digests
     138          for digest_name, digest_cons in _FILE_CONTENT_DIGESTS.items():
     139              if digest_cons is None:
     140                  continue
     141              try:
     142                  data[digest_name] = digest_cons(content).hexdigest()
     143              except ValueError:
     144                  # hash digest not available or blocked by security policy
     145                  pass
     146  
     147          if self.sign:
     148              with open(filename + ".asc", "rb") as f:
     149                  data['gpg_signature'] = (os.path.basename(filename) + ".asc",
     150                                           f.read())
     151  
     152          # set up the authentication
     153          user_pass = (self.username + ":" + self.password).encode('ascii')
     154          # The exact encoding of the authentication string is debated.
     155          # Anyway PyPI only accepts ascii for both username or password.
     156          auth = "Basic " + standard_b64encode(user_pass).decode('ascii')
     157  
     158          # Build up the MIME payload for the POST data
     159          boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254'
     160          sep_boundary = b'\r\n--' + boundary.encode('ascii')
     161          end_boundary = sep_boundary + b'--\r\n'
     162          body = io.BytesIO()
     163          for key, value in data.items():
     164              title = '\r\nContent-Disposition: form-data; name="%s"' % key
     165              # handle multiple entries for the same name
     166              if not isinstance(value, list):
     167                  value = [value]
     168              for value in value:
     169                  if type(value) is tuple:
     170                      title += '; filename="%s"' % value[0]
     171                      value = value[1]
     172                  else:
     173                      value = str(value).encode('utf-8')
     174                  body.write(sep_boundary)
     175                  body.write(title.encode('utf-8'))
     176                  body.write(b"\r\n\r\n")
     177                  body.write(value)
     178          body.write(end_boundary)
     179          body = body.getvalue()
     180  
     181          msg = "Submitting %s to %s" % (filename, self.repository)
     182          self.announce(msg, log.INFO)
     183  
     184          # build the Request
     185          headers = {
     186              'Content-type': 'multipart/form-data; boundary=%s' % boundary,
     187              'Content-length': str(len(body)),
     188              'Authorization': auth,
     189          }
     190  
     191          request = Request(self.repository, data=body,
     192                            headers=headers)
     193          # send the data
     194          try:
     195              result = urlopen(request)
     196              status = result.getcode()
     197              reason = result.msg
     198          except HTTPError as e:
     199              status = e.code
     200              reason = e.msg
     201          except OSError as e:
     202              self.announce(str(e), log.ERROR)
     203              raise
     204  
     205          if status == 200:
     206              self.announce('Server response (%s): %s' % (status, reason),
     207                            log.INFO)
     208              if self.show_response:
     209                  text = self._read_pypi_response(result)
     210                  msg = '\n'.join(('-' * 75, text, '-' * 75))
     211                  self.announce(msg, log.INFO)
     212          else:
     213              msg = 'Upload failed (%s): %s' % (status, reason)
     214              self.announce(msg, log.ERROR)
     215              raise DistutilsError(msg)