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 msg = ("Must create and upload files in one command " 59 "(e.g. setup.py sdist upload)") 60 raise DistutilsOptionError(msg) 61 for command, pyversion, filename in self.distribution.dist_files: 62 self.upload_file(command, pyversion, filename) 63 64 def upload_file(self, command, pyversion, filename): 65 # Makes sure the repository URL is compliant 66 schema, netloc, url, params, query, fragments = \ 67 urlparse.urlparse(self.repository) 68 if params or query or fragments: 69 raise AssertionError("Incompatible url %s" % self.repository) 70 71 if schema not in ('http', 'https'): 72 raise AssertionError("unsupported schema " + schema) 73 74 # Sign if requested 75 if self.sign: 76 gpg_args = ["gpg", "--detach-sign", "-a", filename] 77 if self.identity: 78 gpg_args[2:2] = ["--local-user", self.identity] 79 spawn(gpg_args, 80 dry_run=self.dry_run) 81 82 # Fill in the data - send all the meta-data in case we need to 83 # register a new release 84 f = open(filename,'rb') 85 try: 86 content = f.read() 87 finally: 88 f.close() 89 meta = self.distribution.metadata 90 data = { 91 # action 92 ':action': 'file_upload', 93 'protcol_version': '1', 94 95 # identify release 96 'name': meta.get_name(), 97 'version': meta.get_version(), 98 99 # file content 100 'content': (os.path.basename(filename),content), 101 'filetype': command, 102 'pyversion': pyversion, 103 'md5_digest': md5(content).hexdigest(), 104 105 # additional meta-data 106 'metadata_version' : '1.0', 107 'summary': meta.get_description(), 108 'home_page': meta.get_url(), 109 'author': meta.get_contact(), 110 'author_email': meta.get_contact_email(), 111 'license': meta.get_licence(), 112 'description': meta.get_long_description(), 113 'keywords': meta.get_keywords(), 114 'platform': meta.get_platforms(), 115 'classifiers': meta.get_classifiers(), 116 'download_url': meta.get_download_url(), 117 # PEP 314 118 'provides': meta.get_provides(), 119 'requires': meta.get_requires(), 120 'obsoletes': meta.get_obsoletes(), 121 } 122 comment = '' 123 if command == 'bdist_rpm': 124 dist, version, id = platform.dist() 125 if dist: 126 comment = 'built for %s %s' % (dist, version) 127 elif command == 'bdist_dumb': 128 comment = 'built for %s' % platform.platform(terse=1) 129 data['comment'] = comment 130 131 if self.sign: 132 data['gpg_signature'] = (os.path.basename(filename) + ".asc", 133 open(filename+".asc").read()) 134 135 # set up the authentication 136 auth = "Basic " + standard_b64encode(self.username + ":" + 137 self.password) 138 139 # Build up the MIME payload for the POST data 140 boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254' 141 sep_boundary = '\r\n--' + boundary 142 end_boundary = sep_boundary + '--\r\n' 143 body = StringIO.StringIO() 144 for key, value in data.items(): 145 # handle multiple entries for the same name 146 if not isinstance(value, list): 147 value = [value] 148 for value in value: 149 if isinstance(value, tuple): 150 fn = ';filename="%s"' % value[0] 151 value = value[1] 152 else: 153 fn = "" 154 155 body.write(sep_boundary) 156 body.write('\r\nContent-Disposition: form-data; name="%s"' % key) 157 body.write(fn) 158 body.write("\r\n\r\n") 159 body.write(value) 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