1 """distutils.command.upload 2 3 Implements the Distutils 'upload' subcommand (upload package to PyPI).""" 4 import os 5 import socket 6 import platform 7 from urllib2 import urlopen, Request, HTTPError 8 from base64 import standard_b64encode 9 import urlparse 10 import cStringIO as StringIO 11 from hashlib import md5 12 13 from distutils.errors import DistutilsOptionError 14 from distutils.core import PyPIRCCommand 15 from distutils.spawn import spawn 16 from distutils import log 17 18 class 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 = '\n--' + boundary 140 end_boundary = sep_boundary + '--' 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('\nContent-Disposition: form-data; name="%s"'%key) 155 body.write(fn) 156 body.write("\n\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.write("\n") 162 body = body.getvalue() 163 164 self.announce("Submitting %s to %s" % (filename, self.repository), log.INFO) 165 166 # build the Request 167 headers = {'Content-type': 168 'multipart/form-data; boundary=%s' % boundary, 169 'Content-length': str(len(body)), 170 'Authorization': auth} 171 172 request = Request(self.repository, data=body, 173 headers=headers) 174 # send the data 175 try: 176 result = urlopen(request) 177 status = result.getcode() 178 reason = result.msg 179 if self.show_response: 180 msg = '\n'.join(('-' * 75, r.read(), '-' * 75)) 181 self.announce(msg, log.INFO) 182 except socket.error, e: 183 self.announce(str(e), log.ERROR) 184 return 185 except HTTPError, e: 186 status = e.code 187 reason = e.msg 188 189 if status == 200: 190 self.announce('Server response (%s): %s' % (status, reason), 191 log.INFO) 192 else: 193 self.announce('Upload failed (%s): %s' % (status, reason), 194 log.ERROR) 195