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
13import StringIO
14from warnings import warn
15
16from distutils.core import PyPIRCCommand
17from distutils import log
18
19class register(PyPIRCCommand):
20
21    description = ("register the distribution with the Python package index")
22    user_options = PyPIRCCommand.user_options + [
23        ('list-classifiers', None,
24         'list the valid Trove classifiers'),
25        ('strict', None ,
26         'Will stop the registering if the meta-data are not fully compliant')
27        ]
28    boolean_options = PyPIRCCommand.boolean_options + [
29        'verify', 'list-classifiers', 'strict']
30
31    sub_commands = [('check', lambda self: True)]
32
33    def initialize_options(self):
34        PyPIRCCommand.initialize_options(self)
35        self.list_classifiers = 0
36        self.strict = 0
37
38    def finalize_options(self):
39        PyPIRCCommand.finalize_options(self)
40        # setting options for the `check` subcommand
41        check_options = {'strict': ('register', self.strict),
42                         'restructuredtext': ('register', 1)}
43        self.distribution.command_options['check'] = check_options
44
45    def run(self):
46        self.finalize_options()
47        self._set_config()
48
49        # Run sub commands
50        for cmd_name in self.get_sub_commands():
51            self.run_command(cmd_name)
52
53        if self.dry_run:
54            self.verify_metadata()
55        elif self.list_classifiers:
56            self.classifiers()
57        else:
58            self.send_metadata()
59
60    def check_metadata(self):
61        """Deprecated API."""
62        warn("distutils.command.register.check_metadata is deprecated, \
63              use the check command instead", PendingDeprecationWarning)
64        check = self.distribution.get_command_obj('check')
65        check.ensure_finalized()
66        check.strict = self.strict
67        check.restructuredtext = 1
68        check.run()
69
70    def _set_config(self):
71        ''' Reads the configuration file and set attributes.
72        '''
73        config = self._read_pypirc()
74        if config != {}:
75            self.username = config['username']
76            self.password = config['password']
77            self.repository = config['repository']
78            self.realm = config['realm']
79            self.has_config = True
80        else:
81            if self.repository not in ('pypi', self.DEFAULT_REPOSITORY):
82                raise ValueError('%s not found in .pypirc' % self.repository)
83            if self.repository == 'pypi':
84                self.repository = self.DEFAULT_REPOSITORY
85            self.has_config = False
86
87    def classifiers(self):
88        ''' Fetch the list of classifiers from the server.
89        '''
90        response = urllib2.urlopen(self.repository+'?:action=list_classifiers')
91        log.info(response.read())
92
93    def verify_metadata(self):
94        ''' Send the metadata to the package index server to be checked.
95        '''
96        # send the info to the server and report the result
97        (code, result) = self.post_to_server(self.build_post_data('verify'))
98        log.info('Server response (%s): %s' % (code, result))
99
100
101    def send_metadata(self):
102        ''' Send the metadata to the package index server.
103
104            Well, do the following:
105            1. figure who the user is, and then
106            2. send the data as a Basic auth'ed POST.
107
108            First we try to read the username/password from $HOME/.pypirc,
109            which is a ConfigParser-formatted file with a section
110            [distutils] containing username and password entries (both
111            in clear text). Eg:
112
113                [distutils]
114                index-servers =
115                    pypi
116
117                [pypi]
118                username: fred
119                password: sekrit
120
121            Otherwise, to figure who the user is, we offer the user three
122            choices:
123
124             1. use existing login,
125             2. register as a new user, or
126             3. set the password to a random string and email the user.
127
128        '''
129        # see if we can short-cut and get the username/password from the
130        # config
131        if self.has_config:
132            choice = '1'
133            username = self.username
134            password = self.password
135        else:
136            choice = 'x'
137            username = password = ''
138
139        # get the user's login info
140        choices = '1 2 3 4'.split()
141        while choice not in choices:
142            self.announce('''\
143We need to know who you are, so please choose either:
144 1. use your existing login,
145 2. register as a new user,
146 3. have the server generate a new password for you (and email it to you), or
147 4. quit
148Your selection [default 1]: ''', log.INFO)
149
150            choice = raw_input()
151            if not choice:
152                choice = '1'
153            elif choice not in choices:
154                print 'Please choose one of the four options!'
155
156        if choice == '1':
157            # get the username and password
158            while not username:
159                username = raw_input('Username: ')
160            while not password:
161                password = getpass.getpass('Password: ')
162
163            # set up the authentication
164            auth = urllib2.HTTPPasswordMgr()
165            host = urlparse.urlparse(self.repository)[1]
166            auth.add_password(self.realm, host, username, password)
167            # send the info to the server and report the result
168            code, result = self.post_to_server(self.build_post_data('submit'),
169                auth)
170            self.announce('Server response (%s): %s' % (code, result),
171                          log.INFO)
172
173            # possibly save the login
174            if code == 200:
175                if self.has_config:
176                    # sharing the password in the distribution instance
177                    # so the upload command can reuse it
178                    self.distribution.password = password
179                else:
180                    self.announce(('I can store your PyPI login so future '
181                                   'submissions will be faster.'), log.INFO)
182                    self.announce('(the login will be stored in %s)' % \
183                                  self._get_rc_file(), log.INFO)
184                    choice = 'X'
185                    while choice.lower() not in 'yn':
186                        choice = raw_input('Save your login (y/N)?')
187                        if not choice:
188                            choice = 'n'
189                    if choice.lower() == 'y':
190                        self._store_pypirc(username, password)
191
192        elif choice == '2':
193            data = {':action': 'user'}
194            data['name'] = data['password'] = data['email'] = ''
195            data['confirm'] = None
196            while not data['name']:
197                data['name'] = raw_input('Username: ')
198            while data['password'] != data['confirm']:
199                while not data['password']:
200                    data['password'] = getpass.getpass('Password: ')
201                while not data['confirm']:
202                    data['confirm'] = getpass.getpass(' Confirm: ')
203                if data['password'] != data['confirm']:
204                    data['password'] = ''
205                    data['confirm'] = None
206                    print "Password and confirm don't match!"
207            while not data['email']:
208                data['email'] = raw_input('   EMail: ')
209            code, result = self.post_to_server(data)
210            if code != 200:
211                log.info('Server response (%s): %s' % (code, result))
212            else:
213                log.info('You will receive an email shortly.')
214                log.info(('Follow the instructions in it to '
215                          'complete registration.'))
216        elif choice == '3':
217            data = {':action': 'password_reset'}
218            data['email'] = ''
219            while not data['email']:
220                data['email'] = raw_input('Your email address: ')
221            code, result = self.post_to_server(data)
222            log.info('Server response (%s): %s' % (code, result))
223
224    def build_post_data(self, action):
225        # figure the data to send - the metadata plus some additional
226        # information used by the package server
227        meta = self.distribution.metadata
228        data = {
229            ':action': action,
230            'metadata_version' : '1.0',
231            'name': meta.get_name(),
232            'version': meta.get_version(),
233            'summary': meta.get_description(),
234            'home_page': meta.get_url(),
235            'author': meta.get_contact(),
236            'author_email': meta.get_contact_email(),
237            'license': meta.get_licence(),
238            'description': meta.get_long_description(),
239            'keywords': meta.get_keywords(),
240            'platform': meta.get_platforms(),
241            'classifiers': meta.get_classifiers(),
242            'download_url': meta.get_download_url(),
243            # PEP 314
244            'provides': meta.get_provides(),
245            'requires': meta.get_requires(),
246            'obsoletes': meta.get_obsoletes(),
247        }
248        if data['provides'] or data['requires'] or data['obsoletes']:
249            data['metadata_version'] = '1.1'
250        return data
251
252    def post_to_server(self, data, auth=None):
253        ''' Post a query to the server, and return a string response.
254        '''
255        if 'name' in data:
256            self.announce('Registering %s to %s' % (data['name'],
257                                                   self.repository),
258                                                   log.INFO)
259        # Build up the MIME payload for the urllib2 POST data
260        boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254'
261        sep_boundary = '\n--' + boundary
262        end_boundary = sep_boundary + '--'
263        body = StringIO.StringIO()
264        for key, value in data.items():
265            # handle multiple entries for the same name
266            if type(value) not in (type([]), type( () )):
267                value = [value]
268            for value in value:
269                body.write(sep_boundary)
270                body.write('\nContent-Disposition: form-data; name="%s"'%key)
271                body.write("\n\n")
272                body.write(value)
273                if value and value[-1] == '\r':
274                    body.write('\n')  # write an extra newline (lurve Macs)
275        body.write(end_boundary)
276        body.write("\n")
277        body = body.getvalue()
278
279        # build the Request
280        headers = {
281            'Content-type': 'multipart/form-data; boundary=%s; charset=utf-8'%boundary,
282            'Content-length': str(len(body))
283        }
284        req = urllib2.Request(self.repository, body, headers)
285
286        # handle HTTP and include the Basic Auth handler
287        opener = urllib2.build_opener(
288            urllib2.HTTPBasicAuthHandler(password_mgr=auth)
289        )
290        data = ''
291        try:
292            result = opener.open(req)
293        except urllib2.HTTPError, e:
294            if self.show_response:
295                data = e.fp.read()
296            result = e.code, e.msg
297        except urllib2.URLError, e:
298            result = 500, str(e)
299        else:
300            if self.show_response:
301                data = result.read()
302            result = 200, 'OK'
303        if self.show_response:
304            dashes = '-' * 75
305            self.announce('%s%s%s' % (dashes, data, dashes))
306
307        return result
308