1"""distutils.command.register
2
3Implements the Distutils 'register' command (register with the repository).
4"""
5
6# created 2002/10/21, Richard Jones
7
8__revision__ = "$Id$"
9
10import urllib2
11import getpass
12import urlparse
13from warnings import warn
14
15from distutils.core import PyPIRCCommand
16from distutils import log
17
18class 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('''\
142We 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
147Your 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