blob: 8e347ce6dc397c3c5a4ece84fbd14b2a7659b5ba [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
10import sys, os, string, urllib2, getpass, urlparse
11import StringIO, ConfigParser
12
13from distutils.core import Command
14from distutils.errors import *
15
16class register(Command):
17
Andrew M. Kuchlinga9ccce32003-03-03 18:26:01 +000018 description = ("register the distribution with the Python package index")
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000019
Andrew M. Kuchlinge12dcd72003-02-18 21:28:20 +000020 DEFAULT_REPOSITORY = 'http://www.python.org/pypi'
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000021
22 user_options = [
23 ('repository=', 'r',
24 "url of repository [default: %s]"%DEFAULT_REPOSITORY),
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000025 ('list-classifiers', None,
26 'list the valid Trove classifiers'),
Andrew M. Kuchling23c98c52003-02-19 13:49:35 +000027 ('show-response', None,
28 'display full response text from server'),
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000029 ]
Andrew M. Kuchling23c98c52003-02-19 13:49:35 +000030 boolean_options = ['verify', 'show-response', 'list-classifiers']
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000031
32 def initialize_options(self):
33 self.repository = None
Andrew M. Kuchling23c98c52003-02-19 13:49:35 +000034 self.show_response = 0
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000035 self.list_classifiers = 0
36
37 def finalize_options(self):
38 if self.repository is None:
39 self.repository = self.DEFAULT_REPOSITORY
40
41 def run(self):
42 self.check_metadata()
Andrew M. Kuchling058a84f2003-04-09 12:35:51 +000043 if self.dry_run:
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +000044 self.verify_metadata()
45 elif self.list_classifiers:
46 self.classifiers()
47 else:
48 self.send_metadata()
49
50 def check_metadata(self):
51 """Ensure that all required elements of meta-data (name, version,
52 URL, (author and author_email) or (maintainer and
53 maintainer_email)) are supplied by the Distribution object; warn if
54 any are missing.
55 """
56 metadata = self.distribution.metadata
57
58 missing = []
59 for attr in ('name', 'version', 'url'):
60 if not (hasattr(metadata, attr) and getattr(metadata, attr)):
61 missing.append(attr)
62
63 if missing:
64 self.warn("missing required meta-data: " +
65 string.join(missing, ", "))
66
67 if metadata.author:
68 if not metadata.author_email:
69 self.warn("missing meta-data: if 'author' supplied, " +
70 "'author_email' must be supplied too")
71 elif metadata.maintainer:
72 if not metadata.maintainer_email:
73 self.warn("missing meta-data: if 'maintainer' supplied, " +
74 "'maintainer_email' must be supplied too")
75 else:
76 self.warn("missing meta-data: either (author and author_email) " +
77 "or (maintainer and maintainer_email) " +
78 "must be supplied")
79
80 def classifiers(self):
81 ''' Fetch the list of classifiers from the server.
82 '''
83 response = urllib2.urlopen(self.repository+'?:action=list_classifiers')
84 print response.read()
85
86 def verify_metadata(self):
87 ''' Send the metadata to the package index server to be checked.
88 '''
89 # send the info to the server and report the result
90 (code, result) = self.post_to_server(self.build_post_data('verify'))
91 print 'Server response (%s): %s'%(code, result)
92
93 def send_metadata(self):
94 ''' Send the metadata to the package index server.
95
96 Well, do the following:
97 1. figure who the user is, and then
98 2. send the data as a Basic auth'ed POST.
99
100 First we try to read the username/password from $HOME/.pypirc,
101 which is a ConfigParser-formatted file with a section
102 [server-login] containing username and password entries (both
103 in clear text). Eg:
104
105 [server-login]
106 username: fred
107 password: sekrit
108
109 Otherwise, to figure who the user is, we offer the user three
110 choices:
111
112 1. use existing login,
113 2. register as a new user, or
114 3. set the password to a random string and email the user.
115
116 '''
117 choice = 'x'
118 username = password = ''
119
120 # see if we can short-cut and get the username/password from the
121 # config
122 config = None
123 if os.environ.has_key('HOME'):
124 rc = os.path.join(os.environ['HOME'], '.pypirc')
125 if os.path.exists(rc):
126 print 'Using PyPI login from %s'%rc
127 config = ConfigParser.ConfigParser()
128 config.read(rc)
129 username = config.get('server-login', 'username')
130 password = config.get('server-login', 'password')
131 choice = '1'
132
133 # get the user's login info
134 choices = '1 2 3 4'.split()
135 while choice not in choices:
136 print '''We need to know who you are, so please choose either:
137 1. use your existing login,
138 2. register as a new user,
139 3. have the server generate a new password for you (and email it to you), or
140 4. quit
141Your selection [default 1]: ''',
142 choice = raw_input()
143 if not choice:
144 choice = '1'
145 elif choice not in choices:
146 print 'Please choose one of the four options!'
147
148 if choice == '1':
149 # get the username and password
150 while not username:
151 username = raw_input('Username: ')
152 while not password:
153 password = getpass.getpass('Password: ')
154
155 # set up the authentication
156 auth = urllib2.HTTPPasswordMgr()
157 host = urlparse.urlparse(self.repository)[1]
158 auth.add_password('pypi', host, username, password)
159
160 # send the info to the server and report the result
161 code, result = self.post_to_server(self.build_post_data('submit'),
162 auth)
163 print 'Server response (%s): %s'%(code, result)
164
165 # possibly save the login
166 if os.environ.has_key('HOME') and config is None and code == 200:
167 rc = os.path.join(os.environ['HOME'], '.pypirc')
168 print 'I can store your PyPI login so future submissions will be faster.'
169 print '(the login will be stored in %s)'%rc
170 choice = 'X'
171 while choice.lower() not in 'yn':
172 choice = raw_input('Save your login (y/N)?')
173 if not choice:
174 choice = 'n'
175 if choice.lower() == 'y':
176 f = open(rc, 'w')
177 f.write('[server-login]\nusername:%s\npassword:%s\n'%(
178 username, password))
179 f.close()
180 try:
181 os.chmod(rc, 0600)
182 except:
183 pass
184 elif choice == '2':
185 data = {':action': 'user'}
186 data['name'] = data['password'] = data['email'] = ''
187 data['confirm'] = None
188 while not data['name']:
189 data['name'] = raw_input('Username: ')
190 while data['password'] != data['confirm']:
191 while not data['password']:
192 data['password'] = getpass.getpass('Password: ')
193 while not data['confirm']:
194 data['confirm'] = getpass.getpass(' Confirm: ')
195 if data['password'] != data['confirm']:
196 data['password'] = ''
197 data['confirm'] = None
198 print "Password and confirm don't match!"
199 while not data['email']:
200 data['email'] = raw_input(' EMail: ')
201 code, result = self.post_to_server(data)
202 if code != 200:
203 print 'Server response (%s): %s'%(code, result)
204 else:
205 print 'You will receive an email shortly.'
206 print 'Follow the instructions in it to complete registration.'
207 elif choice == '3':
208 data = {':action': 'password_reset'}
209 data['email'] = ''
210 while not data['email']:
211 data['email'] = raw_input('Your email address: ')
212 code, result = self.post_to_server(data)
213 print 'Server response (%s): %s'%(code, result)
214
215 def build_post_data(self, action):
216 # figure the data to send - the metadata plus some additional
217 # information used by the package server
218 meta = self.distribution.metadata
219 data = {
220 ':action': action,
221 'metadata_version' : '1.0',
222 'name': meta.get_name(),
223 'version': meta.get_version(),
224 'summary': meta.get_description(),
225 'home_page': meta.get_url(),
226 'author': meta.get_contact(),
227 'author_email': meta.get_contact_email(),
228 'license': meta.get_licence(),
229 'description': meta.get_long_description(),
230 'keywords': meta.get_keywords(),
231 'platform': meta.get_platforms(),
Andrew M. Kuchling23c98c52003-02-19 13:49:35 +0000232 'classifiers': meta.get_classifiers(),
Andrew M. Kuchling80be59b2003-02-19 14:27:21 +0000233 'download_url': meta.get_download_url(),
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000234 }
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000235 return data
236
237 def post_to_server(self, data, auth=None):
238 ''' Post a query to the server, and return a string response.
239 '''
240
241 # Build up the MIME payload for the urllib2 POST data
242 boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254'
243 sep_boundary = '\n--' + boundary
244 end_boundary = sep_boundary + '--'
245 body = StringIO.StringIO()
246 for key, value in data.items():
247 # handle multiple entries for the same name
248 if type(value) != type([]):
249 value = [value]
250 for value in value:
251 value = str(value)
252 body.write(sep_boundary)
253 body.write('\nContent-Disposition: form-data; name="%s"'%key)
254 body.write("\n\n")
255 body.write(value)
256 if value and value[-1] == '\r':
257 body.write('\n') # write an extra newline (lurve Macs)
258 body.write(end_boundary)
259 body.write("\n")
260 body = body.getvalue()
261
262 # build the Request
263 headers = {
264 'Content-type': 'multipart/form-data; boundary=%s'%boundary,
265 'Content-length': str(len(body))
266 }
267 req = urllib2.Request(self.repository, body, headers)
268
269 # handle HTTP and include the Basic Auth handler
270 opener = urllib2.build_opener(
271 urllib2.HTTPBasicAuthHandler(password_mgr=auth)
272 )
273 data = ''
274 try:
275 result = opener.open(req)
276 except urllib2.HTTPError, e:
Andrew M. Kuchling23c98c52003-02-19 13:49:35 +0000277 if self.show_response:
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000278 data = e.fp.read()
279 result = e.code, e.msg
280 except urllib2.URLError, e:
281 result = 500, str(e)
282 else:
Andrew M. Kuchling23c98c52003-02-19 13:49:35 +0000283 if self.show_response:
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000284 data = result.read()
285 result = 200, 'OK'
Andrew M. Kuchling23c98c52003-02-19 13:49:35 +0000286 if self.show_response:
Andrew M. Kuchling51a6a4c2003-01-03 15:29:28 +0000287 print '-'*75, data, '-'*75
288 return result
289