blob: bdf5f8f09ccd85074326ad254427883badcbf899 [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
8__revision__ = "$Id$"
9
Jeremy Hylton1afc1692008-06-18 20:49:58 +000010import os, string, getpass
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +000011import io
Jeremy Hylton1afc1692008-06-18 20:49:58 +000012import urllib.parse, urllib.request
Tarek Ziadé5af55c62009-05-16 16:52:13 +000013from warnings import warn
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000014
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +000015from distutils.core import PyPIRCCommand
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000016from distutils.errors import *
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +000017from distutils import log
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000018
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +000019class register(PyPIRCCommand):
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000020
Andrew M. Kuchlinga9ccce32003-03-03 18:26:01 +000021 description = ("register the distribution with the Python package index")
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +000022 user_options = PyPIRCCommand.user_options + [
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000023 ('list-classifiers', None,
24 'list the valid Trove classifiers'),
Tarek Ziadé5af55c62009-05-16 16:52:13 +000025 ('strict', None ,
26 'Will stop the registering if the meta-data are not fully compliant')
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000027 ]
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +000028 boolean_options = PyPIRCCommand.boolean_options + [
Tarek Ziadé5af55c62009-05-16 16:52:13 +000029 'verify', 'list-classifiers', 'strict']
30
31 sub_commands = [('check', lambda self: True)]
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000032
33 def initialize_options(self):
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +000034 PyPIRCCommand.initialize_options(self)
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000035 self.list_classifiers = 0
Tarek Ziadé5af55c62009-05-16 16:52:13 +000036 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
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000044
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000045 def run(self):
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +000046 self.finalize_options()
47 self._set_config()
Tarek Ziadé5af55c62009-05-16 16:52:13 +000048
49 # Run sub commands
50 for cmd_name in self.get_sub_commands():
51 self.run_command(cmd_name)
52
Andrew M. Kuchling058a84f2003-04-09 12:35:51 +000053 if self.dry_run:
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000054 self.verify_metadata()
55 elif self.list_classifiers:
56 self.classifiers()
57 else:
58 self.send_metadata()
59
60 def check_metadata(self):
Tarek Ziadé5af55c62009-05-16 16:52:13 +000061 """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()
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000069
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +000070 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
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000087 def classifiers(self):
88 ''' Fetch the list of classifiers from the server.
89 '''
Jeremy Hylton1afc1692008-06-18 20:49:58 +000090 url = self.repository+'?:action=list_classifiers'
91 response = urllib.request.urlopen(url)
Tarek Ziadébaf51802009-03-31 21:37:16 +000092 log.info(response.read())
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000093
94 def verify_metadata(self):
95 ''' Send the metadata to the package index server to be checked.
96 '''
97 # send the info to the server and report the result
98 (code, result) = self.post_to_server(self.build_post_data('verify'))
Tarek Ziadébaf51802009-03-31 21:37:16 +000099 log.info('Server response (%s): %s' % (code, result))
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +0000100
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000101 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
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +0000110 [distutils] containing username and password entries (both
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000111 in clear text). Eg:
112
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +0000113 [distutils]
114 index-servers =
115 pypi
116
117 [pypi]
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000118 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 '''
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000129 # see if we can short-cut and get the username/password from the
130 # config
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +0000131 if self.has_config:
132 choice = '1'
133 username = self.username
134 password = self.password
135 else:
136 choice = 'x'
137 username = password = ''
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000138
139 # get the user's login info
140 choices = '1 2 3 4'.split()
141 while choice not in choices:
Benjamin Peterson92035012008-12-27 16:00:54 +0000142 self.announce('''\
143We need to know who you are, so please choose either:
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000144 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
Benjamin Peterson92035012008-12-27 16:00:54 +0000148Your selection [default 1]: ''', log.INFO)
Benjamin Peterson467a7bd2008-12-27 17:00:44 +0000149 choice = input()
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000150 if not choice:
151 choice = '1'
152 elif choice not in choices:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000153 print('Please choose one of the four options!')
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000154
155 if choice == '1':
156 # get the username and password
157 while not username:
Martin v. Löwis2d1ca2d2008-11-20 16:21:55 +0000158 username = input('Username: ')
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000159 while not password:
160 password = getpass.getpass('Password: ')
161
162 # set up the authentication
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000163 auth = urllib.request.HTTPPasswordMgr()
164 host = urllib.parse.urlparse(self.repository)[1]
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +0000165 auth.add_password(self.realm, host, username, password)
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000166 # send the info to the server and report the result
167 code, result = self.post_to_server(self.build_post_data('submit'),
168 auth)
Benjamin Peterson92035012008-12-27 16:00:54 +0000169 self.announce('Server response (%s): %s' % (code, result),
170 log.INFO)
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000171
172 # possibly save the login
Tarek Ziadé13f7c3b2009-01-09 00:15:45 +0000173 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 = input('Save your login (y/N)?')
186 if not choice:
187 choice = 'n'
188 if choice.lower() == 'y':
189 self._store_pypirc(username, password)
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +0000190
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000191 elif choice == '2':
192 data = {':action': 'user'}
193 data['name'] = data['password'] = data['email'] = ''
194 data['confirm'] = None
195 while not data['name']:
Martin v. Löwis2d1ca2d2008-11-20 16:21:55 +0000196 data['name'] = input('Username: ')
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000197 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
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000205 print("Password and confirm don't match!")
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000206 while not data['email']:
Martin v. Löwis2d1ca2d2008-11-20 16:21:55 +0000207 data['email'] = input(' EMail: ')
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000208 code, result = self.post_to_server(data)
209 if code != 200:
Tarek Ziadébaf51802009-03-31 21:37:16 +0000210 log.info('Server response (%s): %s' % (code, result))
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000211 else:
Tarek Ziadébaf51802009-03-31 21:37:16 +0000212 log.info('You will receive an email shortly.')
213 log.info(('Follow the instructions in it to '
214 'complete registration.'))
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000215 elif choice == '3':
216 data = {':action': 'password_reset'}
217 data['email'] = ''
218 while not data['email']:
Martin v. Löwis2d1ca2d2008-11-20 16:21:55 +0000219 data['email'] = input('Your email address: ')
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000220 code, result = self.post_to_server(data)
Tarek Ziadébaf51802009-03-31 21:37:16 +0000221 log.info('Server response (%s): %s' % (code, result))
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000222
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(),
Andrew M. Kuchling23c98c52003-02-19 13:49:35 +0000240 'classifiers': meta.get_classifiers(),
Andrew M. Kuchling80be59b2003-02-19 14:27:21 +0000241 'download_url': meta.get_download_url(),
Fred Drakedb7b0022005-03-20 22:19:47 +0000242 # PEP 314
243 'provides': meta.get_provides(),
244 'requires': meta.get_requires(),
245 'obsoletes': meta.get_obsoletes(),
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000246 }
Fred Drakedb7b0022005-03-20 22:19:47 +0000247 if data['provides'] or data['requires'] or data['obsoletes']:
248 data['metadata_version'] = '1.1'
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000249 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 '''
Tarek Ziadébaf51802009-03-31 21:37:16 +0000254 if 'name' in data:
255 self.announce('Registering %s to %s' % (data['name'],
256 self.repository),
257 log.INFO)
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000258 # Build up the MIME payload for the urllib2 POST data
259 boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254'
260 sep_boundary = '\n--' + boundary
261 end_boundary = sep_boundary + '--'
Guido van Rossum34d19282007-08-09 01:03:29 +0000262 body = io.StringIO()
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000263 for key, value in data.items():
264 # handle multiple entries for the same name
Thomas Wouters89f507f2006-12-13 04:49:30 +0000265 if type(value) not in (type([]), type( () )):
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000266 value = [value]
267 for value in value:
Martin v. Löwis2d1ca2d2008-11-20 16:21:55 +0000268 value = str(value)
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000269 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")
Martin v. Löwis2d1ca2d2008-11-20 16:21:55 +0000277 body = body.getvalue().encode("utf-8")
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000278
279 # build the Request
280 headers = {
Walter Dörwalda6e8a4a2005-03-31 13:57:38 +0000281 'Content-type': 'multipart/form-data; boundary=%s; charset=utf-8'%boundary,
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000282 'Content-length': str(len(body))
283 }
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000284 req = urllib.request.Request(self.repository, body, headers)
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000285
286 # handle HTTP and include the Basic Auth handler
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000287 opener = urllib.request.build_opener(
288 urllib.request.HTTPBasicAuthHandler(password_mgr=auth)
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000289 )
290 data = ''
291 try:
292 result = opener.open(req)
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000293 except urllib.error.HTTPError as e:
Andrew M. Kuchling23c98c52003-02-19 13:49:35 +0000294 if self.show_response:
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000295 data = e.fp.read()
296 result = e.code, e.msg
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000297 except urllib.error.URLError as e:
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000298 result = 500, str(e)
299 else:
Andrew M. Kuchling23c98c52003-02-19 13:49:35 +0000300 if self.show_response:
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000301 data = result.read()
302 result = 200, 'OK'
Andrew M. Kuchling23c98c52003-02-19 13:49:35 +0000303 if self.show_response:
Tarek Ziadébaf51802009-03-31 21:37:16 +0000304 dashes = '-' * 75
305 self.announce('%s%s%s' % (dashes, data, dashes))
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000306 return result