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