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