upload.py 6.9 KB

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