• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""distutils.command.register
2
3Implements the Distutils 'register' command (register with the repository).
4"""
5
6# created 2002/10/21, Richard Jones
7
8import getpass
9import io
10import urllib.parse, urllib.request
11from warnings import warn
12
13from distutils.core import PyPIRCCommand
14from distutils.errors import *
15from distutils import log
16
17class 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('''\
141We 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
146Your 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