upload_2.py 6.7 KB

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