Home | History | Annotate | Download | only in command
      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