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