You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

196 lines
6.7 KiB

4 years ago
  1. import io
  2. import os
  3. import hashlib
  4. import getpass
  5. import platform
  6. from base64 import standard_b64encode
  7. from distutils import log
  8. from distutils.command import upload as orig
  9. from distutils.spawn import spawn
  10. from distutils.errors import DistutilsError
  11. from setuptools.extern.six.moves.urllib.request import urlopen, Request
  12. from setuptools.extern.six.moves.urllib.error import HTTPError
  13. from setuptools.extern.six.moves.urllib.parse import urlparse
  14. class upload(orig.upload):
  15. """
  16. Override default upload behavior to obtain password
  17. in a variety of different ways.
  18. """
  19. def run(self):
  20. try:
  21. orig.upload.run(self)
  22. finally:
  23. self.announce(
  24. "WARNING: Uploading via this command is deprecated, use twine "
  25. "to upload instead (https://pypi.org/p/twine/)",
  26. log.WARN
  27. )
  28. def finalize_options(self):
  29. orig.upload.finalize_options(self)
  30. self.username = (
  31. self.username or
  32. getpass.getuser()
  33. )
  34. # Attempt to obtain password. Short circuit evaluation at the first
  35. # sign of success.
  36. self.password = (
  37. self.password or
  38. self._load_password_from_keyring() or
  39. self._prompt_for_password()
  40. )
  41. def upload_file(self, command, pyversion, filename):
  42. # Makes sure the repository URL is compliant
  43. schema, netloc, url, params, query, fragments = \
  44. urlparse(self.repository)
  45. if params or query or fragments:
  46. raise AssertionError("Incompatible url %s" % self.repository)
  47. if schema not in ('http', 'https'):
  48. raise AssertionError("unsupported schema " + schema)
  49. # Sign if requested
  50. if self.sign:
  51. gpg_args = ["gpg", "--detach-sign", "-a", filename]
  52. if self.identity:
  53. gpg_args[2:2] = ["--local-user", self.identity]
  54. spawn(gpg_args,
  55. dry_run=self.dry_run)
  56. # Fill in the data - send all the meta-data in case we need to
  57. # register a new release
  58. with open(filename, 'rb') as f:
  59. content = f.read()
  60. meta = self.distribution.metadata
  61. data = {
  62. # action
  63. ':action': 'file_upload',
  64. 'protocol_version': '1',
  65. # identify release
  66. 'name': meta.get_name(),
  67. 'version': meta.get_version(),
  68. # file content
  69. 'content': (os.path.basename(filename),content),
  70. 'filetype': command,
  71. 'pyversion': pyversion,
  72. 'md5_digest': hashlib.md5(content).hexdigest(),
  73. # additional meta-data
  74. 'metadata_version': str(meta.get_metadata_version()),
  75. 'summary': meta.get_description(),
  76. 'home_page': meta.get_url(),
  77. 'author': meta.get_contact(),
  78. 'author_email': meta.get_contact_email(),
  79. 'license': meta.get_licence(),
  80. 'description': meta.get_long_description(),
  81. 'keywords': meta.get_keywords(),
  82. 'platform': meta.get_platforms(),
  83. 'classifiers': meta.get_classifiers(),
  84. 'download_url': meta.get_download_url(),
  85. # PEP 314
  86. 'provides': meta.get_provides(),
  87. 'requires': meta.get_requires(),
  88. 'obsoletes': meta.get_obsoletes(),
  89. }
  90. data['comment'] = ''
  91. if self.sign:
  92. data['gpg_signature'] = (os.path.basename(filename) + ".asc",
  93. open(filename+".asc", "rb").read())
  94. # set up the authentication
  95. user_pass = (self.username + ":" + self.password).encode('ascii')
  96. # The exact encoding of the authentication string is debated.
  97. # Anyway PyPI only accepts ascii for both username or password.
  98. auth = "Basic " + standard_b64encode(user_pass).decode('ascii')
  99. # Build up the MIME payload for the POST data
  100. boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254'
  101. sep_boundary = b'\r\n--' + boundary.encode('ascii')
  102. end_boundary = sep_boundary + b'--\r\n'
  103. body = io.BytesIO()
  104. for key, value in data.items():
  105. title = '\r\nContent-Disposition: form-data; name="%s"' % key
  106. # handle multiple entries for the same name
  107. if not isinstance(value, list):
  108. value = [value]
  109. for value in value:
  110. if type(value) is tuple:
  111. title += '; filename="%s"' % value[0]
  112. value = value[1]
  113. else:
  114. value = str(value).encode('utf-8')
  115. body.write(sep_boundary)
  116. body.write(title.encode('utf-8'))
  117. body.write(b"\r\n\r\n")
  118. body.write(value)
  119. body.write(end_boundary)
  120. body = body.getvalue()
  121. msg = "Submitting %s to %s" % (filename, self.repository)
  122. self.announce(msg, log.INFO)
  123. # build the Request
  124. headers = {
  125. 'Content-type': 'multipart/form-data; boundary=%s' % boundary,
  126. 'Content-length': str(len(body)),
  127. 'Authorization': auth,
  128. }
  129. request = Request(self.repository, data=body,
  130. headers=headers)
  131. # send the data
  132. try:
  133. result = urlopen(request)
  134. status = result.getcode()
  135. reason = result.msg
  136. except HTTPError as e:
  137. status = e.code
  138. reason = e.msg
  139. except OSError as e:
  140. self.announce(str(e), log.ERROR)
  141. raise
  142. if status == 200:
  143. self.announce('Server response (%s): %s' % (status, reason),
  144. log.INFO)
  145. if self.show_response:
  146. text = getattr(self, '_read_pypi_response',
  147. lambda x: None)(result)
  148. if text is not None:
  149. msg = '\n'.join(('-' * 75, text, '-' * 75))
  150. self.announce(msg, log.INFO)
  151. else:
  152. msg = 'Upload failed (%s): %s' % (status, reason)
  153. self.announce(msg, log.ERROR)
  154. raise DistutilsError(msg)
  155. def _load_password_from_keyring(self):
  156. """
  157. Attempt to load password from keyring. Suppress Exceptions.
  158. """
  159. try:
  160. keyring = __import__('keyring')
  161. return keyring.get_password(self.repository, self.username)
  162. except Exception:
  163. pass
  164. def _prompt_for_password(self):
  165. """
  166. Prompt for a password on the tty. Suppress Exceptions.
  167. """
  168. try:
  169. return getpass.getpass()
  170. except (Exception, KeyboardInterrupt):
  171. pass