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