17db96d56Sopenharmony_ci"""distutils.command.register
27db96d56Sopenharmony_ci
37db96d56Sopenharmony_ciImplements the Distutils 'register' command (register with the repository).
47db96d56Sopenharmony_ci"""
57db96d56Sopenharmony_ci
67db96d56Sopenharmony_ci# created 2002/10/21, Richard Jones
77db96d56Sopenharmony_ci
87db96d56Sopenharmony_ciimport getpass
97db96d56Sopenharmony_ciimport io
107db96d56Sopenharmony_ciimport urllib.parse, urllib.request
117db96d56Sopenharmony_cifrom warnings import warn
127db96d56Sopenharmony_ci
137db96d56Sopenharmony_cifrom distutils.core import PyPIRCCommand
147db96d56Sopenharmony_cifrom distutils.errors import *
157db96d56Sopenharmony_cifrom distutils import log
167db96d56Sopenharmony_ci
177db96d56Sopenharmony_ciclass register(PyPIRCCommand):
187db96d56Sopenharmony_ci
197db96d56Sopenharmony_ci    description = ("register the distribution with the Python package index")
207db96d56Sopenharmony_ci    user_options = PyPIRCCommand.user_options + [
217db96d56Sopenharmony_ci        ('list-classifiers', None,
227db96d56Sopenharmony_ci         'list the valid Trove classifiers'),
237db96d56Sopenharmony_ci        ('strict', None ,
247db96d56Sopenharmony_ci         'Will stop the registering if the meta-data are not fully compliant')
257db96d56Sopenharmony_ci        ]
267db96d56Sopenharmony_ci    boolean_options = PyPIRCCommand.boolean_options + [
277db96d56Sopenharmony_ci        'verify', 'list-classifiers', 'strict']
287db96d56Sopenharmony_ci
297db96d56Sopenharmony_ci    sub_commands = [('check', lambda self: True)]
307db96d56Sopenharmony_ci
317db96d56Sopenharmony_ci    def initialize_options(self):
327db96d56Sopenharmony_ci        PyPIRCCommand.initialize_options(self)
337db96d56Sopenharmony_ci        self.list_classifiers = 0
347db96d56Sopenharmony_ci        self.strict = 0
357db96d56Sopenharmony_ci
367db96d56Sopenharmony_ci    def finalize_options(self):
377db96d56Sopenharmony_ci        PyPIRCCommand.finalize_options(self)
387db96d56Sopenharmony_ci        # setting options for the `check` subcommand
397db96d56Sopenharmony_ci        check_options = {'strict': ('register', self.strict),
407db96d56Sopenharmony_ci                         'restructuredtext': ('register', 1)}
417db96d56Sopenharmony_ci        self.distribution.command_options['check'] = check_options
427db96d56Sopenharmony_ci
437db96d56Sopenharmony_ci    def run(self):
447db96d56Sopenharmony_ci        self.finalize_options()
457db96d56Sopenharmony_ci        self._set_config()
467db96d56Sopenharmony_ci
477db96d56Sopenharmony_ci        # Run sub commands
487db96d56Sopenharmony_ci        for cmd_name in self.get_sub_commands():
497db96d56Sopenharmony_ci            self.run_command(cmd_name)
507db96d56Sopenharmony_ci
517db96d56Sopenharmony_ci        if self.dry_run:
527db96d56Sopenharmony_ci            self.verify_metadata()
537db96d56Sopenharmony_ci        elif self.list_classifiers:
547db96d56Sopenharmony_ci            self.classifiers()
557db96d56Sopenharmony_ci        else:
567db96d56Sopenharmony_ci            self.send_metadata()
577db96d56Sopenharmony_ci
587db96d56Sopenharmony_ci    def check_metadata(self):
597db96d56Sopenharmony_ci        """Deprecated API."""
607db96d56Sopenharmony_ci        warn("distutils.command.register.check_metadata is deprecated, \
617db96d56Sopenharmony_ci              use the check command instead", PendingDeprecationWarning)
627db96d56Sopenharmony_ci        check = self.distribution.get_command_obj('check')
637db96d56Sopenharmony_ci        check.ensure_finalized()
647db96d56Sopenharmony_ci        check.strict = self.strict
657db96d56Sopenharmony_ci        check.restructuredtext = 1
667db96d56Sopenharmony_ci        check.run()
677db96d56Sopenharmony_ci
687db96d56Sopenharmony_ci    def _set_config(self):
697db96d56Sopenharmony_ci        ''' Reads the configuration file and set attributes.
707db96d56Sopenharmony_ci        '''
717db96d56Sopenharmony_ci        config = self._read_pypirc()
727db96d56Sopenharmony_ci        if config != {}:
737db96d56Sopenharmony_ci            self.username = config['username']
747db96d56Sopenharmony_ci            self.password = config['password']
757db96d56Sopenharmony_ci            self.repository = config['repository']
767db96d56Sopenharmony_ci            self.realm = config['realm']
777db96d56Sopenharmony_ci            self.has_config = True
787db96d56Sopenharmony_ci        else:
797db96d56Sopenharmony_ci            if self.repository not in ('pypi', self.DEFAULT_REPOSITORY):
807db96d56Sopenharmony_ci                raise ValueError('%s not found in .pypirc' % self.repository)
817db96d56Sopenharmony_ci            if self.repository == 'pypi':
827db96d56Sopenharmony_ci                self.repository = self.DEFAULT_REPOSITORY
837db96d56Sopenharmony_ci            self.has_config = False
847db96d56Sopenharmony_ci
857db96d56Sopenharmony_ci    def classifiers(self):
867db96d56Sopenharmony_ci        ''' Fetch the list of classifiers from the server.
877db96d56Sopenharmony_ci        '''
887db96d56Sopenharmony_ci        url = self.repository+'?:action=list_classifiers'
897db96d56Sopenharmony_ci        response = urllib.request.urlopen(url)
907db96d56Sopenharmony_ci        log.info(self._read_pypi_response(response))
917db96d56Sopenharmony_ci
927db96d56Sopenharmony_ci    def verify_metadata(self):
937db96d56Sopenharmony_ci        ''' Send the metadata to the package index server to be checked.
947db96d56Sopenharmony_ci        '''
957db96d56Sopenharmony_ci        # send the info to the server and report the result
967db96d56Sopenharmony_ci        (code, result) = self.post_to_server(self.build_post_data('verify'))
977db96d56Sopenharmony_ci        log.info('Server response (%s): %s', code, result)
987db96d56Sopenharmony_ci
997db96d56Sopenharmony_ci    def send_metadata(self):
1007db96d56Sopenharmony_ci        ''' Send the metadata to the package index server.
1017db96d56Sopenharmony_ci
1027db96d56Sopenharmony_ci            Well, do the following:
1037db96d56Sopenharmony_ci            1. figure who the user is, and then
1047db96d56Sopenharmony_ci            2. send the data as a Basic auth'ed POST.
1057db96d56Sopenharmony_ci
1067db96d56Sopenharmony_ci            First we try to read the username/password from $HOME/.pypirc,
1077db96d56Sopenharmony_ci            which is a ConfigParser-formatted file with a section
1087db96d56Sopenharmony_ci            [distutils] containing username and password entries (both
1097db96d56Sopenharmony_ci            in clear text). Eg:
1107db96d56Sopenharmony_ci
1117db96d56Sopenharmony_ci                [distutils]
1127db96d56Sopenharmony_ci                index-servers =
1137db96d56Sopenharmony_ci                    pypi
1147db96d56Sopenharmony_ci
1157db96d56Sopenharmony_ci                [pypi]
1167db96d56Sopenharmony_ci                username: fred
1177db96d56Sopenharmony_ci                password: sekrit
1187db96d56Sopenharmony_ci
1197db96d56Sopenharmony_ci            Otherwise, to figure who the user is, we offer the user three
1207db96d56Sopenharmony_ci            choices:
1217db96d56Sopenharmony_ci
1227db96d56Sopenharmony_ci             1. use existing login,
1237db96d56Sopenharmony_ci             2. register as a new user, or
1247db96d56Sopenharmony_ci             3. set the password to a random string and email the user.
1257db96d56Sopenharmony_ci
1267db96d56Sopenharmony_ci        '''
1277db96d56Sopenharmony_ci        # see if we can short-cut and get the username/password from the
1287db96d56Sopenharmony_ci        # config
1297db96d56Sopenharmony_ci        if self.has_config:
1307db96d56Sopenharmony_ci            choice = '1'
1317db96d56Sopenharmony_ci            username = self.username
1327db96d56Sopenharmony_ci            password = self.password
1337db96d56Sopenharmony_ci        else:
1347db96d56Sopenharmony_ci            choice = 'x'
1357db96d56Sopenharmony_ci            username = password = ''
1367db96d56Sopenharmony_ci
1377db96d56Sopenharmony_ci        # get the user's login info
1387db96d56Sopenharmony_ci        choices = '1 2 3 4'.split()
1397db96d56Sopenharmony_ci        while choice not in choices:
1407db96d56Sopenharmony_ci            self.announce('''\
1417db96d56Sopenharmony_ciWe need to know who you are, so please choose either:
1427db96d56Sopenharmony_ci 1. use your existing login,
1437db96d56Sopenharmony_ci 2. register as a new user,
1447db96d56Sopenharmony_ci 3. have the server generate a new password for you (and email it to you), or
1457db96d56Sopenharmony_ci 4. quit
1467db96d56Sopenharmony_ciYour selection [default 1]: ''', log.INFO)
1477db96d56Sopenharmony_ci            choice = input()
1487db96d56Sopenharmony_ci            if not choice:
1497db96d56Sopenharmony_ci                choice = '1'
1507db96d56Sopenharmony_ci            elif choice not in choices:
1517db96d56Sopenharmony_ci                print('Please choose one of the four options!')
1527db96d56Sopenharmony_ci
1537db96d56Sopenharmony_ci        if choice == '1':
1547db96d56Sopenharmony_ci            # get the username and password
1557db96d56Sopenharmony_ci            while not username:
1567db96d56Sopenharmony_ci                username = input('Username: ')
1577db96d56Sopenharmony_ci            while not password:
1587db96d56Sopenharmony_ci                password = getpass.getpass('Password: ')
1597db96d56Sopenharmony_ci
1607db96d56Sopenharmony_ci            # set up the authentication
1617db96d56Sopenharmony_ci            auth = urllib.request.HTTPPasswordMgr()
1627db96d56Sopenharmony_ci            host = urllib.parse.urlparse(self.repository)[1]
1637db96d56Sopenharmony_ci            auth.add_password(self.realm, host, username, password)
1647db96d56Sopenharmony_ci            # send the info to the server and report the result
1657db96d56Sopenharmony_ci            code, result = self.post_to_server(self.build_post_data('submit'),
1667db96d56Sopenharmony_ci                auth)
1677db96d56Sopenharmony_ci            self.announce('Server response (%s): %s' % (code, result),
1687db96d56Sopenharmony_ci                          log.INFO)
1697db96d56Sopenharmony_ci
1707db96d56Sopenharmony_ci            # possibly save the login
1717db96d56Sopenharmony_ci            if code == 200:
1727db96d56Sopenharmony_ci                if self.has_config:
1737db96d56Sopenharmony_ci                    # sharing the password in the distribution instance
1747db96d56Sopenharmony_ci                    # so the upload command can reuse it
1757db96d56Sopenharmony_ci                    self.distribution.password = password
1767db96d56Sopenharmony_ci                else:
1777db96d56Sopenharmony_ci                    self.announce(('I can store your PyPI login so future '
1787db96d56Sopenharmony_ci                                   'submissions will be faster.'), log.INFO)
1797db96d56Sopenharmony_ci                    self.announce('(the login will be stored in %s)' % \
1807db96d56Sopenharmony_ci                                  self._get_rc_file(), log.INFO)
1817db96d56Sopenharmony_ci                    choice = 'X'
1827db96d56Sopenharmony_ci                    while choice.lower() not in 'yn':
1837db96d56Sopenharmony_ci                        choice = input('Save your login (y/N)?')
1847db96d56Sopenharmony_ci                        if not choice:
1857db96d56Sopenharmony_ci                            choice = 'n'
1867db96d56Sopenharmony_ci                    if choice.lower() == 'y':
1877db96d56Sopenharmony_ci                        self._store_pypirc(username, password)
1887db96d56Sopenharmony_ci
1897db96d56Sopenharmony_ci        elif choice == '2':
1907db96d56Sopenharmony_ci            data = {':action': 'user'}
1917db96d56Sopenharmony_ci            data['name'] = data['password'] = data['email'] = ''
1927db96d56Sopenharmony_ci            data['confirm'] = None
1937db96d56Sopenharmony_ci            while not data['name']:
1947db96d56Sopenharmony_ci                data['name'] = input('Username: ')
1957db96d56Sopenharmony_ci            while data['password'] != data['confirm']:
1967db96d56Sopenharmony_ci                while not data['password']:
1977db96d56Sopenharmony_ci                    data['password'] = getpass.getpass('Password: ')
1987db96d56Sopenharmony_ci                while not data['confirm']:
1997db96d56Sopenharmony_ci                    data['confirm'] = getpass.getpass(' Confirm: ')
2007db96d56Sopenharmony_ci                if data['password'] != data['confirm']:
2017db96d56Sopenharmony_ci                    data['password'] = ''
2027db96d56Sopenharmony_ci                    data['confirm'] = None
2037db96d56Sopenharmony_ci                    print("Password and confirm don't match!")
2047db96d56Sopenharmony_ci            while not data['email']:
2057db96d56Sopenharmony_ci                data['email'] = input('   EMail: ')
2067db96d56Sopenharmony_ci            code, result = self.post_to_server(data)
2077db96d56Sopenharmony_ci            if code != 200:
2087db96d56Sopenharmony_ci                log.info('Server response (%s): %s', code, result)
2097db96d56Sopenharmony_ci            else:
2107db96d56Sopenharmony_ci                log.info('You will receive an email shortly.')
2117db96d56Sopenharmony_ci                log.info(('Follow the instructions in it to '
2127db96d56Sopenharmony_ci                          'complete registration.'))
2137db96d56Sopenharmony_ci        elif choice == '3':
2147db96d56Sopenharmony_ci            data = {':action': 'password_reset'}
2157db96d56Sopenharmony_ci            data['email'] = ''
2167db96d56Sopenharmony_ci            while not data['email']:
2177db96d56Sopenharmony_ci                data['email'] = input('Your email address: ')
2187db96d56Sopenharmony_ci            code, result = self.post_to_server(data)
2197db96d56Sopenharmony_ci            log.info('Server response (%s): %s', code, result)
2207db96d56Sopenharmony_ci
2217db96d56Sopenharmony_ci    def build_post_data(self, action):
2227db96d56Sopenharmony_ci        # figure the data to send - the metadata plus some additional
2237db96d56Sopenharmony_ci        # information used by the package server
2247db96d56Sopenharmony_ci        meta = self.distribution.metadata
2257db96d56Sopenharmony_ci        data = {
2267db96d56Sopenharmony_ci            ':action': action,
2277db96d56Sopenharmony_ci            'metadata_version' : '1.0',
2287db96d56Sopenharmony_ci            'name': meta.get_name(),
2297db96d56Sopenharmony_ci            'version': meta.get_version(),
2307db96d56Sopenharmony_ci            'summary': meta.get_description(),
2317db96d56Sopenharmony_ci            'home_page': meta.get_url(),
2327db96d56Sopenharmony_ci            'author': meta.get_contact(),
2337db96d56Sopenharmony_ci            'author_email': meta.get_contact_email(),
2347db96d56Sopenharmony_ci            'license': meta.get_licence(),
2357db96d56Sopenharmony_ci            'description': meta.get_long_description(),
2367db96d56Sopenharmony_ci            'keywords': meta.get_keywords(),
2377db96d56Sopenharmony_ci            'platform': meta.get_platforms(),
2387db96d56Sopenharmony_ci            'classifiers': meta.get_classifiers(),
2397db96d56Sopenharmony_ci            'download_url': meta.get_download_url(),
2407db96d56Sopenharmony_ci            # PEP 314
2417db96d56Sopenharmony_ci            'provides': meta.get_provides(),
2427db96d56Sopenharmony_ci            'requires': meta.get_requires(),
2437db96d56Sopenharmony_ci            'obsoletes': meta.get_obsoletes(),
2447db96d56Sopenharmony_ci        }
2457db96d56Sopenharmony_ci        if data['provides'] or data['requires'] or data['obsoletes']:
2467db96d56Sopenharmony_ci            data['metadata_version'] = '1.1'
2477db96d56Sopenharmony_ci        return data
2487db96d56Sopenharmony_ci
2497db96d56Sopenharmony_ci    def post_to_server(self, data, auth=None):
2507db96d56Sopenharmony_ci        ''' Post a query to the server, and return a string response.
2517db96d56Sopenharmony_ci        '''
2527db96d56Sopenharmony_ci        if 'name' in data:
2537db96d56Sopenharmony_ci            self.announce('Registering %s to %s' % (data['name'],
2547db96d56Sopenharmony_ci                                                    self.repository),
2557db96d56Sopenharmony_ci                                                    log.INFO)
2567db96d56Sopenharmony_ci        # Build up the MIME payload for the urllib2 POST data
2577db96d56Sopenharmony_ci        boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254'
2587db96d56Sopenharmony_ci        sep_boundary = '\n--' + boundary
2597db96d56Sopenharmony_ci        end_boundary = sep_boundary + '--'
2607db96d56Sopenharmony_ci        body = io.StringIO()
2617db96d56Sopenharmony_ci        for key, value in data.items():
2627db96d56Sopenharmony_ci            # handle multiple entries for the same name
2637db96d56Sopenharmony_ci            if type(value) not in (type([]), type( () )):
2647db96d56Sopenharmony_ci                value = [value]
2657db96d56Sopenharmony_ci            for value in value:
2667db96d56Sopenharmony_ci                value = str(value)
2677db96d56Sopenharmony_ci                body.write(sep_boundary)
2687db96d56Sopenharmony_ci                body.write('\nContent-Disposition: form-data; name="%s"'%key)
2697db96d56Sopenharmony_ci                body.write("\n\n")
2707db96d56Sopenharmony_ci                body.write(value)
2717db96d56Sopenharmony_ci                if value and value[-1] == '\r':
2727db96d56Sopenharmony_ci                    body.write('\n')  # write an extra newline (lurve Macs)
2737db96d56Sopenharmony_ci        body.write(end_boundary)
2747db96d56Sopenharmony_ci        body.write("\n")
2757db96d56Sopenharmony_ci        body = body.getvalue().encode("utf-8")
2767db96d56Sopenharmony_ci
2777db96d56Sopenharmony_ci        # build the Request
2787db96d56Sopenharmony_ci        headers = {
2797db96d56Sopenharmony_ci            'Content-type': 'multipart/form-data; boundary=%s; charset=utf-8'%boundary,
2807db96d56Sopenharmony_ci            'Content-length': str(len(body))
2817db96d56Sopenharmony_ci        }
2827db96d56Sopenharmony_ci        req = urllib.request.Request(self.repository, body, headers)
2837db96d56Sopenharmony_ci
2847db96d56Sopenharmony_ci        # handle HTTP and include the Basic Auth handler
2857db96d56Sopenharmony_ci        opener = urllib.request.build_opener(
2867db96d56Sopenharmony_ci            urllib.request.HTTPBasicAuthHandler(password_mgr=auth)
2877db96d56Sopenharmony_ci        )
2887db96d56Sopenharmony_ci        data = ''
2897db96d56Sopenharmony_ci        try:
2907db96d56Sopenharmony_ci            result = opener.open(req)
2917db96d56Sopenharmony_ci        except urllib.error.HTTPError as e:
2927db96d56Sopenharmony_ci            if self.show_response:
2937db96d56Sopenharmony_ci                data = e.fp.read()
2947db96d56Sopenharmony_ci            result = e.code, e.msg
2957db96d56Sopenharmony_ci        except urllib.error.URLError as e:
2967db96d56Sopenharmony_ci            result = 500, str(e)
2977db96d56Sopenharmony_ci        else:
2987db96d56Sopenharmony_ci            if self.show_response:
2997db96d56Sopenharmony_ci                data = self._read_pypi_response(result)
3007db96d56Sopenharmony_ci            result = 200, 'OK'
3017db96d56Sopenharmony_ci        if self.show_response:
3027db96d56Sopenharmony_ci            msg = '\n'.join(('-' * 75, data, '-' * 75))
3037db96d56Sopenharmony_ci            self.announce(msg, log.INFO)
3047db96d56Sopenharmony_ci        return result
305