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