/Lib/distutils/command/upload.py

http://unladen-swallow.googlecode.com/ · Python · 185 lines · 180 code · 2 blank · 3 comment · 12 complexity · e149a9da546ee94900093e047f3e355c MD5 · raw file

  1. """distutils.command.upload
  2. Implements the Distutils 'upload' subcommand (upload package to PyPI)."""
  3. from distutils.errors import *
  4. from distutils.core import PyPIRCCommand
  5. from distutils.spawn import spawn
  6. from distutils import log
  7. from hashlib import md5
  8. import os
  9. import socket
  10. import platform
  11. import httplib
  12. import base64
  13. import urlparse
  14. import cStringIO as StringIO
  15. from ConfigParser import ConfigParser
  16. class upload(PyPIRCCommand):
  17. description = "upload binary package to PyPI"
  18. user_options = PyPIRCCommand.user_options + [
  19. ('sign', 's',
  20. 'sign files to upload using gpg'),
  21. ('identity=', 'i', 'GPG identity used to sign files'),
  22. ]
  23. boolean_options = PyPIRCCommand.boolean_options + ['sign']
  24. def initialize_options(self):
  25. PyPIRCCommand.initialize_options(self)
  26. self.username = ''
  27. self.password = ''
  28. self.show_response = 0
  29. self.sign = False
  30. self.identity = None
  31. def finalize_options(self):
  32. PyPIRCCommand.finalize_options(self)
  33. if self.identity and not self.sign:
  34. raise DistutilsOptionError(
  35. "Must use --sign for --identity to have meaning"
  36. )
  37. config = self._read_pypirc()
  38. if config != {}:
  39. self.username = config['username']
  40. self.password = config['password']
  41. self.repository = config['repository']
  42. self.realm = config['realm']
  43. def run(self):
  44. if not self.distribution.dist_files:
  45. raise DistutilsOptionError("No dist file created in earlier command")
  46. for command, pyversion, filename in self.distribution.dist_files:
  47. self.upload_file(command, pyversion, filename)
  48. def upload_file(self, command, pyversion, filename):
  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. content = open(filename,'rb').read()
  59. meta = self.distribution.metadata
  60. data = {
  61. # action
  62. ':action': 'file_upload',
  63. 'protcol_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': md5(content).hexdigest(),
  72. # additional meta-data
  73. 'metadata_version' : '1.0',
  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. comment = ''
  90. if command == 'bdist_rpm':
  91. dist, version, id = platform.dist()
  92. if dist:
  93. comment = 'built for %s %s' % (dist, version)
  94. elif command == 'bdist_dumb':
  95. comment = 'built for %s' % platform.platform(terse=1)
  96. data['comment'] = comment
  97. if self.sign:
  98. data['gpg_signature'] = (os.path.basename(filename) + ".asc",
  99. open(filename+".asc").read())
  100. # set up the authentication
  101. auth = "Basic " + base64.encodestring(self.username + ":" + self.password).strip()
  102. # Build up the MIME payload for the POST data
  103. boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254'
  104. sep_boundary = '\n--' + boundary
  105. end_boundary = sep_boundary + '--'
  106. body = StringIO.StringIO()
  107. for key, value in data.items():
  108. # handle multiple entries for the same name
  109. if type(value) != type([]):
  110. value = [value]
  111. for value in value:
  112. if type(value) is tuple:
  113. fn = ';filename="%s"' % value[0]
  114. value = value[1]
  115. else:
  116. fn = ""
  117. value = str(value)
  118. body.write(sep_boundary)
  119. body.write('\nContent-Disposition: form-data; name="%s"'%key)
  120. body.write(fn)
  121. body.write("\n\n")
  122. body.write(value)
  123. if value and value[-1] == '\r':
  124. body.write('\n') # write an extra newline (lurve Macs)
  125. body.write(end_boundary)
  126. body.write("\n")
  127. body = body.getvalue()
  128. self.announce("Submitting %s to %s" % (filename, self.repository), log.INFO)
  129. # build the Request
  130. # We can't use urllib2 since we need to send the Basic
  131. # auth right with the first request
  132. schema, netloc, url, params, query, fragments = \
  133. urlparse.urlparse(self.repository)
  134. assert not params and not query and not fragments
  135. if schema == 'http':
  136. http = httplib.HTTPConnection(netloc)
  137. elif schema == 'https':
  138. http = httplib.HTTPSConnection(netloc)
  139. else:
  140. raise AssertionError, "unsupported schema "+schema
  141. data = ''
  142. loglevel = log.INFO
  143. try:
  144. http.connect()
  145. http.putrequest("POST", url)
  146. http.putheader('Content-type',
  147. 'multipart/form-data; boundary=%s'%boundary)
  148. http.putheader('Content-length', str(len(body)))
  149. http.putheader('Authorization', auth)
  150. http.endheaders()
  151. http.send(body)
  152. except socket.error, e:
  153. self.announce(str(e), log.ERROR)
  154. return
  155. r = http.getresponse()
  156. if r.status == 200:
  157. self.announce('Server response (%s): %s' % (r.status, r.reason),
  158. log.INFO)
  159. else:
  160. self.announce('Upload failed (%s): %s' % (r.status, r.reason),
  161. log.ERROR)
  162. if self.show_response:
  163. print '-'*75, r.read(), '-'*75