upload.py 7.2 KB

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