blob: 456d50d519a1eac832a869e95f1eb8d3f94dd4bf [file] [log] [blame]
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +00001"""distutils.command.register
2
3Implements the Distutils 'register' command (register with the repository).
4"""
5
6# created 2002/10/21, Richard Jones
7
Serhiy Storchakaccd047e2016-04-25 00:12:32 +03008import getpass
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +00009import io
Tarek Ziadé36797272010-07-22 12:50:05 +000010import urllib.parse, urllib.request
Tarek Ziadé5af55c62009-05-16 16:52:13 +000011from warnings import warn
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000012
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +000013from distutils.core import PyPIRCCommand
Tarek Ziadé36797272010-07-22 12:50:05 +000014from distutils.errors import *
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +000015from distutils import log
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000016
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +000017class register(PyPIRCCommand):
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000018
Andrew M. Kuchlinga9ccce32003-03-03 18:26:01 +000019 description = ("register the distribution with the Python package index")
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +000020 user_options = PyPIRCCommand.user_options + [
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000021 ('list-classifiers', None,
22 'list the valid Trove classifiers'),
Tarek Ziadé5af55c62009-05-16 16:52:13 +000023 ('strict', None ,
24 'Will stop the registering if the meta-data are not fully compliant')
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000025 ]
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +000026 boolean_options = PyPIRCCommand.boolean_options + [
Tarek Ziadé5af55c62009-05-16 16:52:13 +000027 'verify', 'list-classifiers', 'strict']
28
29 sub_commands = [('check', lambda self: True)]
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000030
31 def initialize_options(self):
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +000032 PyPIRCCommand.initialize_options(self)
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000033 self.list_classifiers = 0
Tarek Ziadé5af55c62009-05-16 16:52:13 +000034 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
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000042
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000043 def run(self):
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +000044 self.finalize_options()
45 self._set_config()
Tarek Ziadé5af55c62009-05-16 16:52:13 +000046
47 # Run sub commands
48 for cmd_name in self.get_sub_commands():
49 self.run_command(cmd_name)
50
Andrew M. Kuchling058a84f2003-04-09 12:35:51 +000051 if self.dry_run:
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000052 self.verify_metadata()
53 elif self.list_classifiers:
54 self.classifiers()
55 else:
56 self.send_metadata()
57
58 def check_metadata(self):
Tarek Ziadé5af55c62009-05-16 16:52:13 +000059 """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()
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000067
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +000068 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
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000085 def classifiers(self):
86 ''' Fetch the list of classifiers from the server.
87 '''
Jeremy Hylton1afc1692008-06-18 20:49:58 +000088 url = self.repository+'?:action=list_classifiers'
89 response = urllib.request.urlopen(url)
Antoine Pitrou335a5122013-12-22 18:13:51 +010090 log.info(self._read_pypi_response(response))
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000091
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'))
Tarek Ziadébaf51802009-03-31 21:37:16 +000097 log.info('Server response (%s): %s' % (code, result))
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +000098
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000099 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
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +0000108 [distutils] containing username and password entries (both
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000109 in clear text). Eg:
110
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +0000111 [distutils]
112 index-servers =
113 pypi
114
115 [pypi]
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000116 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 '''
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000127 # see if we can short-cut and get the username/password from the
128 # config
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +0000129 if self.has_config:
130 choice = '1'
131 username = self.username
132 password = self.password
133 else:
134 choice = 'x'
135 username = password = ''
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000136
137 # get the user's login info
138 choices = '1 2 3 4'.split()
139 while choice not in choices:
Benjamin Peterson92035012008-12-27 16:00:54 +0000140 self.announce('''\
141We need to know who you are, so please choose either:
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000142 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
Benjamin Peterson92035012008-12-27 16:00:54 +0000146Your selection [default 1]: ''', log.INFO)
Benjamin Peterson467a7bd2008-12-27 17:00:44 +0000147 choice = input()
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000148 if not choice:
149 choice = '1'
150 elif choice not in choices:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000151 print('Please choose one of the four options!')
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000152
153 if choice == '1':
154 # get the username and password
155 while not username:
Martin v. Löwis2d1ca2d2008-11-20 16:21:55 +0000156 username = input('Username: ')
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000157 while not password:
158 password = getpass.getpass('Password: ')
159
160 # set up the authentication
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000161 auth = urllib.request.HTTPPasswordMgr()
162 host = urllib.parse.urlparse(self.repository)[1]
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +0000163 auth.add_password(self.realm, host, username, password)
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000164 # send the info to the server and report the result
165 code, result = self.post_to_server(self.build_post_data('submit'),
166 auth)
Benjamin Peterson92035012008-12-27 16:00:54 +0000167 self.announce('Server response (%s): %s' % (code, result),
168 log.INFO)
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000169
170 # possibly save the login
Tarek Ziadé13f7c3b2009-01-09 00:15:45 +0000171 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)
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +0000188
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000189 elif choice == '2':
190 data = {':action': 'user'}
191 data['name'] = data['password'] = data['email'] = ''
192 data['confirm'] = None
193 while not data['name']:
Martin v. Löwis2d1ca2d2008-11-20 16:21:55 +0000194 data['name'] = input('Username: ')
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000195 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
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000203 print("Password and confirm don't match!")
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000204 while not data['email']:
Martin v. Löwis2d1ca2d2008-11-20 16:21:55 +0000205 data['email'] = input(' EMail: ')
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000206 code, result = self.post_to_server(data)
207 if code != 200:
Tarek Ziadébaf51802009-03-31 21:37:16 +0000208 log.info('Server response (%s): %s' % (code, result))
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000209 else:
Tarek Ziadébaf51802009-03-31 21:37:16 +0000210 log.info('You will receive an email shortly.')
211 log.info(('Follow the instructions in it to '
212 'complete registration.'))
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000213 elif choice == '3':
214 data = {':action': 'password_reset'}
215 data['email'] = ''
216 while not data['email']:
Martin v. Löwis2d1ca2d2008-11-20 16:21:55 +0000217 data['email'] = input('Your email address: ')
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000218 code, result = self.post_to_server(data)
Tarek Ziadébaf51802009-03-31 21:37:16 +0000219 log.info('Server response (%s): %s' % (code, result))
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000220
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(),
Andrew M. Kuchling23c98c52003-02-19 13:49:35 +0000238 'classifiers': meta.get_classifiers(),
Andrew M. Kuchling80be59b2003-02-19 14:27:21 +0000239 'download_url': meta.get_download_url(),
Fred Drakedb7b0022005-03-20 22:19:47 +0000240 # PEP 314
241 'provides': meta.get_provides(),
242 'requires': meta.get_requires(),
243 'obsoletes': meta.get_obsoletes(),
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000244 }
Fred Drakedb7b0022005-03-20 22:19:47 +0000245 if data['provides'] or data['requires'] or data['obsoletes']:
246 data['metadata_version'] = '1.1'
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000247 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 '''
Tarek Ziadébaf51802009-03-31 21:37:16 +0000252 if 'name' in data:
253 self.announce('Registering %s to %s' % (data['name'],
254 self.repository),
255 log.INFO)
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000256 # Build up the MIME payload for the urllib2 POST data
257 boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254'
258 sep_boundary = '\n--' + boundary
259 end_boundary = sep_boundary + '--'
Guido van Rossum34d19282007-08-09 01:03:29 +0000260 body = io.StringIO()
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000261 for key, value in data.items():
262 # handle multiple entries for the same name
Thomas Wouters89f507f2006-12-13 04:49:30 +0000263 if type(value) not in (type([]), type( () )):
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000264 value = [value]
265 for value in value:
Martin v. Löwis2d1ca2d2008-11-20 16:21:55 +0000266 value = str(value)
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000267 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")
Martin v. Löwis2d1ca2d2008-11-20 16:21:55 +0000275 body = body.getvalue().encode("utf-8")
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000276
277 # build the Request
278 headers = {
Walter Dörwalda6e8a4a2005-03-31 13:57:38 +0000279 'Content-type': 'multipart/form-data; boundary=%s; charset=utf-8'%boundary,
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000280 'Content-length': str(len(body))
281 }
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000282 req = urllib.request.Request(self.repository, body, headers)
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000283
284 # handle HTTP and include the Basic Auth handler
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000285 opener = urllib.request.build_opener(
286 urllib.request.HTTPBasicAuthHandler(password_mgr=auth)
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000287 )
288 data = ''
289 try:
290 result = opener.open(req)
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000291 except urllib.error.HTTPError as e:
Andrew M. Kuchling23c98c52003-02-19 13:49:35 +0000292 if self.show_response:
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000293 data = e.fp.read()
294 result = e.code, e.msg
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000295 except urllib.error.URLError as e:
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000296 result = 500, str(e)
297 else:
Andrew M. Kuchling23c98c52003-02-19 13:49:35 +0000298 if self.show_response:
Berker Peksag904a2ef2016-06-10 23:00:52 +0300299 data = self._read_pypi_response(result)
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000300 result = 200, 'OK'
Andrew M. Kuchling23c98c52003-02-19 13:49:35 +0000301 if self.show_response:
Berker Peksag904a2ef2016-06-10 23:00:52 +0300302 msg = '\n'.join(('-' * 75, data, '-' * 75))
303 self.announce(msg, log.INFO)
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000304 return result