1"""distutils.command.register 2 3Implements the Distutils 'register' command (register with the repository). 4""" 5 6# created 2002/10/21, Richard Jones 7 8import getpass 9import io 10import urllib.parse, urllib.request 11from warnings import warn 12 13from distutils.core import PyPIRCCommand 14from distutils.errors import * 15from distutils import log 16 17class register(PyPIRCCommand): 18 19 description = ("register the distribution with the Python package index") 20 user_options = PyPIRCCommand.user_options + [ 21 ('list-classifiers', None, 22 'list the valid Trove classifiers'), 23 ('strict', None , 24 'Will stop the registering if the meta-data are not fully compliant') 25 ] 26 boolean_options = PyPIRCCommand.boolean_options + [ 27 'verify', 'list-classifiers', 'strict'] 28 29 sub_commands = [('check', lambda self: True)] 30 31 def initialize_options(self): 32 PyPIRCCommand.initialize_options(self) 33 self.list_classifiers = 0 34 self.strict = 0 35 36 def finalize_options(self): 37 PyPIRCCommand.finalize_options(self) 38 # setting options for the `check` subcommand 39 check_options = {'strict': ('register', self.strict), 40 'restructuredtext': ('register', 1)} 41 self.distribution.command_options['check'] = check_options 42 43 def run(self): 44 self.finalize_options() 45 self._set_config() 46 47 # Run sub commands 48 for cmd_name in self.get_sub_commands(): 49 self.run_command(cmd_name) 50 51 if self.dry_run: 52 self.verify_metadata() 53 elif self.list_classifiers: 54 self.classifiers() 55 else: 56 self.send_metadata() 57 58 def check_metadata(self): 59 """Deprecated API.""" 60 warn("distutils.command.register.check_metadata is deprecated, \ 61 use the check command instead", PendingDeprecationWarning) 62 check = self.distribution.get_command_obj('check') 63 check.ensure_finalized() 64 check.strict = self.strict 65 check.restructuredtext = 1 66 check.run() 67 68 def _set_config(self): 69 ''' Reads the configuration file and set attributes. 70 ''' 71 config = self._read_pypirc() 72 if config != {}: 73 self.username = config['username'] 74 self.password = config['password'] 75 self.repository = config['repository'] 76 self.realm = config['realm'] 77 self.has_config = True 78 else: 79 if self.repository not in ('pypi', self.DEFAULT_REPOSITORY): 80 raise ValueError('%s not found in .pypirc' % self.repository) 81 if self.repository == 'pypi': 82 self.repository = self.DEFAULT_REPOSITORY 83 self.has_config = False 84 85 def classifiers(self): 86 ''' Fetch the list of classifiers from the server. 87 ''' 88 url = self.repository+'?:action=list_classifiers' 89 response = urllib.request.urlopen(url) 90 log.info(self._read_pypi_response(response)) 91 92 def verify_metadata(self): 93 ''' Send the metadata to the package index server to be checked. 94 ''' 95 # send the info to the server and report the result 96 (code, result) = self.post_to_server(self.build_post_data('verify')) 97 log.info('Server response (%s): %s', code, result) 98 99 def send_metadata(self): 100 ''' Send the metadata to the package index server. 101 102 Well, do the following: 103 1. figure who the user is, and then 104 2. send the data as a Basic auth'ed POST. 105 106 First we try to read the username/password from $HOME/.pypirc, 107 which is a ConfigParser-formatted file with a section 108 [distutils] containing username and password entries (both 109 in clear text). Eg: 110 111 [distutils] 112 index-servers = 113 pypi 114 115 [pypi] 116 username: fred 117 password: sekrit 118 119 Otherwise, to figure who the user is, we offer the user three 120 choices: 121 122 1. use existing login, 123 2. register as a new user, or 124 3. set the password to a random string and email the user. 125 126 ''' 127 # see if we can short-cut and get the username/password from the 128 # config 129 if self.has_config: 130 choice = '1' 131 username = self.username 132 password = self.password 133 else: 134 choice = 'x' 135 username = password = '' 136 137 # get the user's login info 138 choices = '1 2 3 4'.split() 139 while choice not in choices: 140 self.announce('''\ 141We need to know who you are, so please choose either: 142 1. use your existing login, 143 2. register as a new user, 144 3. have the server generate a new password for you (and email it to you), or 145 4. quit 146Your selection [default 1]: ''', log.INFO) 147 choice = input() 148 if not choice: 149 choice = '1' 150 elif choice not in choices: 151 print('Please choose one of the four options!') 152 153 if choice == '1': 154 # get the username and password 155 while not username: 156 username = input('Username: ') 157 while not password: 158 password = getpass.getpass('Password: ') 159 160 # set up the authentication 161 auth = urllib.request.HTTPPasswordMgr() 162 host = urllib.parse.urlparse(self.repository)[1] 163 auth.add_password(self.realm, host, username, password) 164 # send the info to the server and report the result 165 code, result = self.post_to_server(self.build_post_data('submit'), 166 auth) 167 self.announce('Server response (%s): %s' % (code, result), 168 log.INFO) 169 170 # possibly save the login 171 if code == 200: 172 if self.has_config: 173 # sharing the password in the distribution instance 174 # so the upload command can reuse it 175 self.distribution.password = password 176 else: 177 self.announce(('I can store your PyPI login so future ' 178 'submissions will be faster.'), log.INFO) 179 self.announce('(the login will be stored in %s)' % \ 180 self._get_rc_file(), log.INFO) 181 choice = 'X' 182 while choice.lower() not in 'yn': 183 choice = input('Save your login (y/N)?') 184 if not choice: 185 choice = 'n' 186 if choice.lower() == 'y': 187 self._store_pypirc(username, password) 188 189 elif choice == '2': 190 data = {':action': 'user'} 191 data['name'] = data['password'] = data['email'] = '' 192 data['confirm'] = None 193 while not data['name']: 194 data['name'] = input('Username: ') 195 while data['password'] != data['confirm']: 196 while not data['password']: 197 data['password'] = getpass.getpass('Password: ') 198 while not data['confirm']: 199 data['confirm'] = getpass.getpass(' Confirm: ') 200 if data['password'] != data['confirm']: 201 data['password'] = '' 202 data['confirm'] = None 203 print("Password and confirm don't match!") 204 while not data['email']: 205 data['email'] = input(' EMail: ') 206 code, result = self.post_to_server(data) 207 if code != 200: 208 log.info('Server response (%s): %s', code, result) 209 else: 210 log.info('You will receive an email shortly.') 211 log.info(('Follow the instructions in it to ' 212 'complete registration.')) 213 elif choice == '3': 214 data = {':action': 'password_reset'} 215 data['email'] = '' 216 while not data['email']: 217 data['email'] = input('Your email address: ') 218 code, result = self.post_to_server(data) 219 log.info('Server response (%s): %s', code, result) 220 221 def build_post_data(self, action): 222 # figure the data to send - the metadata plus some additional 223 # information used by the package server 224 meta = self.distribution.metadata 225 data = { 226 ':action': action, 227 'metadata_version' : '1.0', 228 'name': meta.get_name(), 229 'version': meta.get_version(), 230 'summary': meta.get_description(), 231 'home_page': meta.get_url(), 232 'author': meta.get_contact(), 233 'author_email': meta.get_contact_email(), 234 'license': meta.get_licence(), 235 'description': meta.get_long_description(), 236 'keywords': meta.get_keywords(), 237 'platform': meta.get_platforms(), 238 'classifiers': meta.get_classifiers(), 239 'download_url': meta.get_download_url(), 240 # PEP 314 241 'provides': meta.get_provides(), 242 'requires': meta.get_requires(), 243 'obsoletes': meta.get_obsoletes(), 244 } 245 if data['provides'] or data['requires'] or data['obsoletes']: 246 data['metadata_version'] = '1.1' 247 return data 248 249 def post_to_server(self, data, auth=None): 250 ''' Post a query to the server, and return a string response. 251 ''' 252 if 'name' in data: 253 self.announce('Registering %s to %s' % (data['name'], 254 self.repository), 255 log.INFO) 256 # Build up the MIME payload for the urllib2 POST data 257 boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254' 258 sep_boundary = '\n--' + boundary 259 end_boundary = sep_boundary + '--' 260 body = io.StringIO() 261 for key, value in data.items(): 262 # handle multiple entries for the same name 263 if type(value) not in (type([]), type( () )): 264 value = [value] 265 for value in value: 266 value = str(value) 267 body.write(sep_boundary) 268 body.write('\nContent-Disposition: form-data; name="%s"'%key) 269 body.write("\n\n") 270 body.write(value) 271 if value and value[-1] == '\r': 272 body.write('\n') # write an extra newline (lurve Macs) 273 body.write(end_boundary) 274 body.write("\n") 275 body = body.getvalue().encode("utf-8") 276 277 # build the Request 278 headers = { 279 'Content-type': 'multipart/form-data; boundary=%s; charset=utf-8'%boundary, 280 'Content-length': str(len(body)) 281 } 282 req = urllib.request.Request(self.repository, body, headers) 283 284 # handle HTTP and include the Basic Auth handler 285 opener = urllib.request.build_opener( 286 urllib.request.HTTPBasicAuthHandler(password_mgr=auth) 287 ) 288 data = '' 289 try: 290 result = opener.open(req) 291 except urllib.error.HTTPError as e: 292 if self.show_response: 293 data = e.fp.read() 294 result = e.code, e.msg 295 except urllib.error.URLError as e: 296 result = 500, str(e) 297 else: 298 if self.show_response: 299 data = self._read_pypi_response(result) 300 result = 200, 'OK' 301 if self.show_response: 302 msg = '\n'.join(('-' * 75, data, '-' * 75)) 303 self.announce(msg, log.INFO) 304 return result 305