| 
									
										
										
										
											2011-05-19 13:07:25 +02:00
										 |  |  | """Upload a distribution to a project index.""" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import os | 
					
						
							|  |  |  | import socket | 
					
						
							|  |  |  | import logging | 
					
						
							|  |  |  | import platform | 
					
						
							|  |  |  | import urllib.parse | 
					
						
							|  |  |  | from base64 import standard_b64encode | 
					
						
							|  |  |  | from hashlib import md5 | 
					
						
							|  |  |  | from urllib.error import HTTPError | 
					
						
							|  |  |  | from urllib.request import urlopen, Request | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | from packaging import logger | 
					
						
							|  |  |  | from packaging.errors import PackagingOptionError | 
					
						
							|  |  |  | from packaging.util import (spawn, read_pypirc, DEFAULT_REPOSITORY, | 
					
						
							| 
									
										
										
										
											2011-07-08 16:27:12 +02:00
										 |  |  |                             DEFAULT_REALM, encode_multipart) | 
					
						
							| 
									
										
										
										
											2011-05-19 13:07:25 +02:00
										 |  |  | from packaging.command.cmd import Command | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class upload(Command): | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     description = "upload distribution to PyPI" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     user_options = [ | 
					
						
							|  |  |  |         ('repository=', 'r', | 
					
						
							|  |  |  |          "repository URL [default: %s]" % DEFAULT_REPOSITORY), | 
					
						
							|  |  |  |         ('show-response', None, | 
					
						
							|  |  |  |          "display full response text from server"), | 
					
						
							|  |  |  |         ('sign', 's', | 
					
						
							|  |  |  |          "sign files to upload using gpg"), | 
					
						
							|  |  |  |         ('identity=', 'i', | 
					
						
							|  |  |  |          "GPG identity used to sign files"), | 
					
						
							|  |  |  |         ('upload-docs', None, | 
					
						
							|  |  |  |          "upload documentation too"), | 
					
						
							|  |  |  |         ] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     boolean_options = ['show-response', 'sign'] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def initialize_options(self): | 
					
						
							|  |  |  |         self.repository = None | 
					
						
							|  |  |  |         self.realm = None | 
					
						
							|  |  |  |         self.show_response = False | 
					
						
							|  |  |  |         self.username = '' | 
					
						
							|  |  |  |         self.password = '' | 
					
						
							|  |  |  |         self.show_response = False | 
					
						
							|  |  |  |         self.sign = False | 
					
						
							|  |  |  |         self.identity = None | 
					
						
							|  |  |  |         self.upload_docs = False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def finalize_options(self): | 
					
						
							|  |  |  |         if self.repository is None: | 
					
						
							|  |  |  |             self.repository = DEFAULT_REPOSITORY | 
					
						
							|  |  |  |         if self.realm is None: | 
					
						
							|  |  |  |             self.realm = DEFAULT_REALM | 
					
						
							|  |  |  |         if self.identity and not self.sign: | 
					
						
							|  |  |  |             raise PackagingOptionError( | 
					
						
							|  |  |  |                 "Must use --sign for --identity to have meaning") | 
					
						
							|  |  |  |         config = read_pypirc(self.repository, self.realm) | 
					
						
							|  |  |  |         if config != {}: | 
					
						
							|  |  |  |             self.username = config['username'] | 
					
						
							|  |  |  |             self.password = config['password'] | 
					
						
							|  |  |  |             self.repository = config['repository'] | 
					
						
							|  |  |  |             self.realm = config['realm'] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # getting the password from the distribution | 
					
						
							|  |  |  |         # if previously set by the register command | 
					
						
							|  |  |  |         if not self.password and self.distribution.password: | 
					
						
							|  |  |  |             self.password = self.distribution.password | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def run(self): | 
					
						
							|  |  |  |         if not self.distribution.dist_files: | 
					
						
							|  |  |  |             raise PackagingOptionError( | 
					
						
							|  |  |  |                 "No dist file created in earlier command") | 
					
						
							|  |  |  |         for command, pyversion, filename in self.distribution.dist_files: | 
					
						
							|  |  |  |             self.upload_file(command, pyversion, filename) | 
					
						
							|  |  |  |         if self.upload_docs: | 
					
						
							|  |  |  |             upload_docs = self.get_finalized_command("upload_docs") | 
					
						
							|  |  |  |             upload_docs.repository = self.repository | 
					
						
							|  |  |  |             upload_docs.username = self.username | 
					
						
							|  |  |  |             upload_docs.password = self.password | 
					
						
							|  |  |  |             upload_docs.run() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # XXX to be refactored with register.post_to_server | 
					
						
							|  |  |  |     def upload_file(self, command, pyversion, filename): | 
					
						
							|  |  |  |         # Makes sure the repository URL is compliant | 
					
						
							|  |  |  |         scheme, netloc, url, params, query, fragments = \ | 
					
						
							|  |  |  |             urllib.parse.urlparse(self.repository) | 
					
						
							|  |  |  |         if params or query or fragments: | 
					
						
							|  |  |  |             raise AssertionError("Incompatible url %s" % self.repository) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if scheme not in ('http', 'https'): | 
					
						
							|  |  |  |             raise AssertionError("unsupported scheme " + scheme) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Sign if requested | 
					
						
							|  |  |  |         if self.sign: | 
					
						
							|  |  |  |             gpg_args = ["gpg", "--detach-sign", "-a", filename] | 
					
						
							|  |  |  |             if self.identity: | 
					
						
							|  |  |  |                 gpg_args[2:2] = ["--local-user", self.identity] | 
					
						
							|  |  |  |             spawn(gpg_args, | 
					
						
							|  |  |  |                   dry_run=self.dry_run) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Fill in the data - send all the metadata in case we need to | 
					
						
							|  |  |  |         # register a new release | 
					
						
							|  |  |  |         with open(filename, 'rb') as f: | 
					
						
							|  |  |  |             content = f.read() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         data = self.distribution.metadata.todict() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # extra upload infos | 
					
						
							|  |  |  |         data[':action'] = 'file_upload' | 
					
						
							|  |  |  |         data['protcol_version'] = '1' | 
					
						
							|  |  |  |         data['content'] = (os.path.basename(filename), content) | 
					
						
							|  |  |  |         data['filetype'] = command | 
					
						
							|  |  |  |         data['pyversion'] = pyversion | 
					
						
							|  |  |  |         data['md5_digest'] = md5(content).hexdigest() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if command == 'bdist_dumb': | 
					
						
							|  |  |  |             data['comment'] = 'built for %s' % platform.platform(terse=True) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if self.sign: | 
					
						
							|  |  |  |             with open(filename + '.asc') as fp: | 
					
						
							|  |  |  |                 sig = fp.read() | 
					
						
							|  |  |  |             data['gpg_signature'] = [ | 
					
						
							|  |  |  |                 (os.path.basename(filename) + ".asc", sig)] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # set up the authentication | 
					
						
							|  |  |  |         # The exact encoding of the authentication string is debated. | 
					
						
							|  |  |  |         # Anyway PyPI only accepts ascii for both username or password. | 
					
						
							|  |  |  |         user_pass = (self.username + ":" + self.password).encode('ascii') | 
					
						
							|  |  |  |         auth = b"Basic " + standard_b64encode(user_pass) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Build up the MIME payload for the POST data | 
					
						
							| 
									
										
										
										
											2011-07-08 16:27:12 +02:00
										 |  |  |         files = [] | 
					
						
							|  |  |  |         for key in ('content', 'gpg_signature'): | 
					
						
							|  |  |  |             if key in data: | 
					
						
							|  |  |  |                 filename_, value = data.pop(key) | 
					
						
							|  |  |  |                 files.append((key, filename_, value)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         content_type, body = encode_multipart(data.items(), files) | 
					
						
							| 
									
										
										
										
											2011-05-19 13:07:25 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         logger.info("Submitting %s to %s", filename, self.repository) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # build the Request | 
					
						
							| 
									
										
										
										
											2011-07-08 16:27:12 +02:00
										 |  |  |         headers = {'Content-type': content_type, | 
					
						
							| 
									
										
										
										
											2011-05-19 13:07:25 +02:00
										 |  |  |                    'Content-length': str(len(body)), | 
					
						
							|  |  |  |                    'Authorization': auth} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2011-07-08 16:27:12 +02:00
										 |  |  |         request = Request(self.repository, body, headers) | 
					
						
							| 
									
										
										
										
											2011-05-19 13:07:25 +02:00
										 |  |  |         # send the data | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             result = urlopen(request) | 
					
						
							|  |  |  |             status = result.code | 
					
						
							|  |  |  |             reason = result.msg | 
					
						
							|  |  |  |         except socket.error as e: | 
					
						
							|  |  |  |             logger.error(e) | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         except HTTPError as e: | 
					
						
							|  |  |  |             status = e.code | 
					
						
							|  |  |  |             reason = e.msg | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if status == 200: | 
					
						
							|  |  |  |             logger.info('Server response (%s): %s', status, reason) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             logger.error('Upload failed (%s): %s', status, reason) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if self.show_response and logger.isEnabledFor(logging.INFO): | 
					
						
							|  |  |  |             sep = '-' * 75 | 
					
						
							|  |  |  |             logger.info('%s\n%s\n%s', sep, result.read().decode(), sep) |