blob: c271b18c4c203a40d933cd733f67dc12d52db696 [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
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000013
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +000014from distutils.core import PyPIRCCommand
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000015from distutils.errors import *
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +000016from distutils import log
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000017
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +000018class register(PyPIRCCommand):
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000019
Andrew M. Kuchlinga9ccce32003-03-03 18:26:01 +000020 description = ("register the distribution with the Python package index")
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +000021 user_options = PyPIRCCommand.user_options + [
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000022 ('list-classifiers', None,
23 'list the valid Trove classifiers'),
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000024 ]
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +000025 boolean_options = PyPIRCCommand.boolean_options + [
26 'verify', 'list-classifiers']
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000027
28 def initialize_options(self):
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +000029 PyPIRCCommand.initialize_options(self)
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000030 self.list_classifiers = 0
31
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000032 def run(self):
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +000033 self.finalize_options()
34 self._set_config()
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000035 self.check_metadata()
Andrew M. Kuchling058a84f2003-04-09 12:35:51 +000036 if self.dry_run:
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000037 self.verify_metadata()
38 elif self.list_classifiers:
39 self.classifiers()
40 else:
41 self.send_metadata()
42
43 def check_metadata(self):
44 """Ensure that all required elements of meta-data (name, version,
45 URL, (author and author_email) or (maintainer and
46 maintainer_email)) are supplied by the Distribution object; warn if
47 any are missing.
48 """
49 metadata = self.distribution.metadata
50
51 missing = []
52 for attr in ('name', 'version', 'url'):
53 if not (hasattr(metadata, attr) and getattr(metadata, attr)):
54 missing.append(attr)
55
56 if missing:
57 self.warn("missing required meta-data: " +
Neal Norwitz9d72bb42007-04-17 08:48:32 +000058 ", ".join(missing))
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000059
60 if metadata.author:
61 if not metadata.author_email:
62 self.warn("missing meta-data: if 'author' supplied, " +
63 "'author_email' must be supplied too")
64 elif metadata.maintainer:
65 if not metadata.maintainer_email:
66 self.warn("missing meta-data: if 'maintainer' supplied, " +
67 "'maintainer_email' must be supplied too")
68 else:
69 self.warn("missing meta-data: either (author and author_email) " +
70 "or (maintainer and maintainer_email) " +
71 "must be supplied")
72
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +000073 def _set_config(self):
74 ''' Reads the configuration file and set attributes.
75 '''
76 config = self._read_pypirc()
77 if config != {}:
78 self.username = config['username']
79 self.password = config['password']
80 self.repository = config['repository']
81 self.realm = config['realm']
82 self.has_config = True
83 else:
84 if self.repository not in ('pypi', self.DEFAULT_REPOSITORY):
85 raise ValueError('%s not found in .pypirc' % self.repository)
86 if self.repository == 'pypi':
87 self.repository = self.DEFAULT_REPOSITORY
88 self.has_config = False
89
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000090 def classifiers(self):
91 ''' Fetch the list of classifiers from the server.
92 '''
Jeremy Hylton1afc1692008-06-18 20:49:58 +000093 url = self.repository+'?:action=list_classifiers'
94 response = urllib.request.urlopen(url)
Guido van Rossumbe19ed72007-02-09 05:37:30 +000095 print(response.read())
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000096
97 def verify_metadata(self):
98 ''' Send the metadata to the package index server to be checked.
99 '''
100 # send the info to the server and report the result
101 (code, result) = self.post_to_server(self.build_post_data('verify'))
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000102 print('Server response (%s): %s'%(code, result))
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000103
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +0000104
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000105 def send_metadata(self):
106 ''' Send the metadata to the package index server.
107
108 Well, do the following:
109 1. figure who the user is, and then
110 2. send the data as a Basic auth'ed POST.
111
112 First we try to read the username/password from $HOME/.pypirc,
113 which is a ConfigParser-formatted file with a section
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +0000114 [distutils] containing username and password entries (both
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000115 in clear text). Eg:
116
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +0000117 [distutils]
118 index-servers =
119 pypi
120
121 [pypi]
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000122 username: fred
123 password: sekrit
124
125 Otherwise, to figure who the user is, we offer the user three
126 choices:
127
128 1. use existing login,
129 2. register as a new user, or
130 3. set the password to a random string and email the user.
131
132 '''
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000133 # see if we can short-cut and get the username/password from the
134 # config
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +0000135 if self.has_config:
136 choice = '1'
137 username = self.username
138 password = self.password
139 else:
140 choice = 'x'
141 username = password = ''
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000142
143 # get the user's login info
144 choices = '1 2 3 4'.split()
145 while choice not in choices:
Benjamin Peterson92035012008-12-27 16:00:54 +0000146 self.announce('''\
147We need to know who you are, so please choose either:
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000148 1. use your existing login,
149 2. register as a new user,
150 3. have the server generate a new password for you (and email it to you), or
151 4. quit
Benjamin Peterson92035012008-12-27 16:00:54 +0000152Your selection [default 1]: ''', log.INFO)
Benjamin Peterson467a7bd2008-12-27 17:00:44 +0000153 choice = input()
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000154 if not choice:
155 choice = '1'
156 elif choice not in choices:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000157 print('Please choose one of the four options!')
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000158
159 if choice == '1':
160 # get the username and password
161 while not username:
Martin v. Löwis2d1ca2d2008-11-20 16:21:55 +0000162 username = input('Username: ')
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000163 while not password:
164 password = getpass.getpass('Password: ')
165
166 # set up the authentication
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000167 auth = urllib.request.HTTPPasswordMgr()
168 host = urllib.parse.urlparse(self.repository)[1]
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +0000169 auth.add_password(self.realm, host, username, password)
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000170 # send the info to the server and report the result
171 code, result = self.post_to_server(self.build_post_data('submit'),
172 auth)
Benjamin Peterson92035012008-12-27 16:00:54 +0000173 self.announce('Server response (%s): %s' % (code, result),
174 log.INFO)
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000175
176 # possibly save the login
Tarek Ziadé13f7c3b2009-01-09 00:15:45 +0000177 if code == 200:
178 if self.has_config:
179 # sharing the password in the distribution instance
180 # so the upload command can reuse it
181 self.distribution.password = password
182 else:
183 self.announce(('I can store your PyPI login so future '
184 'submissions will be faster.'), log.INFO)
185 self.announce('(the login will be stored in %s)' % \
186 self._get_rc_file(), log.INFO)
187 choice = 'X'
188 while choice.lower() not in 'yn':
189 choice = input('Save your login (y/N)?')
190 if not choice:
191 choice = 'n'
192 if choice.lower() == 'y':
193 self._store_pypirc(username, password)
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +0000194
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000195 elif choice == '2':
196 data = {':action': 'user'}
197 data['name'] = data['password'] = data['email'] = ''
198 data['confirm'] = None
199 while not data['name']:
Martin v. Löwis2d1ca2d2008-11-20 16:21:55 +0000200 data['name'] = input('Username: ')
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000201 while data['password'] != data['confirm']:
202 while not data['password']:
203 data['password'] = getpass.getpass('Password: ')
204 while not data['confirm']:
205 data['confirm'] = getpass.getpass(' Confirm: ')
206 if data['password'] != data['confirm']:
207 data['password'] = ''
208 data['confirm'] = None
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000209 print("Password and confirm don't match!")
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000210 while not data['email']:
Martin v. Löwis2d1ca2d2008-11-20 16:21:55 +0000211 data['email'] = input(' EMail: ')
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000212 code, result = self.post_to_server(data)
213 if code != 200:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000214 print('Server response (%s): %s'%(code, result))
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000215 else:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000216 print('You will receive an email shortly.')
217 print('Follow the instructions in it to complete registration.')
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000218 elif choice == '3':
219 data = {':action': 'password_reset'}
220 data['email'] = ''
221 while not data['email']:
Martin v. Löwis2d1ca2d2008-11-20 16:21:55 +0000222 data['email'] = input('Your email address: ')
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000223 code, result = self.post_to_server(data)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000224 print('Server response (%s): %s'%(code, result))
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000225
226 def build_post_data(self, action):
227 # figure the data to send - the metadata plus some additional
228 # information used by the package server
229 meta = self.distribution.metadata
230 data = {
231 ':action': action,
232 'metadata_version' : '1.0',
233 'name': meta.get_name(),
234 'version': meta.get_version(),
235 'summary': meta.get_description(),
236 'home_page': meta.get_url(),
237 'author': meta.get_contact(),
238 'author_email': meta.get_contact_email(),
239 'license': meta.get_licence(),
240 'description': meta.get_long_description(),
241 'keywords': meta.get_keywords(),
242 'platform': meta.get_platforms(),
Andrew M. Kuchling23c98c52003-02-19 13:49:35 +0000243 'classifiers': meta.get_classifiers(),
Andrew M. Kuchling80be59b2003-02-19 14:27:21 +0000244 'download_url': meta.get_download_url(),
Fred Drakedb7b0022005-03-20 22:19:47 +0000245 # PEP 314
246 'provides': meta.get_provides(),
247 'requires': meta.get_requires(),
248 'obsoletes': meta.get_obsoletes(),
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000249 }
Fred Drakedb7b0022005-03-20 22:19:47 +0000250 if data['provides'] or data['requires'] or data['obsoletes']:
251 data['metadata_version'] = '1.1'
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000252 return data
253
254 def post_to_server(self, data, auth=None):
255 ''' Post a query to the server, and return a string response.
256 '''
Alexandre Vassalotti5f8ced22008-05-16 00:03:33 +0000257 self.announce('Registering %s to %s' % (data['name'],
258 self.repository), log.INFO)
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000259 # Build up the MIME payload for the urllib2 POST data
260 boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254'
261 sep_boundary = '\n--' + boundary
262 end_boundary = sep_boundary + '--'
Guido van Rossum34d19282007-08-09 01:03:29 +0000263 body = io.StringIO()
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000264 for key, value in data.items():
265 # handle multiple entries for the same name
Thomas Wouters89f507f2006-12-13 04:49:30 +0000266 if type(value) not in (type([]), type( () )):
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000267 value = [value]
268 for value in value:
Martin v. Löwis2d1ca2d2008-11-20 16:21:55 +0000269 value = str(value)
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000270 body.write(sep_boundary)
271 body.write('\nContent-Disposition: form-data; name="%s"'%key)
272 body.write("\n\n")
273 body.write(value)
274 if value and value[-1] == '\r':
275 body.write('\n') # write an extra newline (lurve Macs)
276 body.write(end_boundary)
277 body.write("\n")
Martin v. Löwis2d1ca2d2008-11-20 16:21:55 +0000278 body = body.getvalue().encode("utf-8")
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000279
280 # build the Request
281 headers = {
Walter Dörwalda6e8a4a2005-03-31 13:57:38 +0000282 'Content-type': 'multipart/form-data; boundary=%s; charset=utf-8'%boundary,
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000283 'Content-length': str(len(body))
284 }
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000285 req = urllib.request.Request(self.repository, body, headers)
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000286
287 # handle HTTP and include the Basic Auth handler
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000288 opener = urllib.request.build_opener(
289 urllib.request.HTTPBasicAuthHandler(password_mgr=auth)
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000290 )
291 data = ''
292 try:
293 result = opener.open(req)
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000294 except urllib.error.HTTPError as e:
Andrew M. Kuchling23c98c52003-02-19 13:49:35 +0000295 if self.show_response:
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000296 data = e.fp.read()
297 result = e.code, e.msg
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000298 except urllib.error.URLError as e:
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000299 result = 500, str(e)
300 else:
Andrew M. Kuchling23c98c52003-02-19 13:49:35 +0000301 if self.show_response:
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000302 data = result.read()
303 result = 200, 'OK'
Andrew M. Kuchling23c98c52003-02-19 13:49:35 +0000304 if self.show_response:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000305 print('-'*75, data, '-'*75)
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000306 return result