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 import getpass
      9 import io
     10 import urllib.parse, urllib.request
     11 from warnings import warn
     12 
     13 from distutils.core import PyPIRCCommand
     14 from distutils.errors import *
     15 from distutils import log
     16 
     17 class 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('''\
    141 We 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
    146 Your 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