]>
Commit | Line | Data |
---|---|---|
1e4a197e RD |
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 | __revision__ = "$Id$" | |
9 | ||
10 | import sys, os, string, urllib2, getpass, urlparse | |
11 | import StringIO, ConfigParser | |
12 | ||
13 | from distutils.core import Command | |
14 | from distutils.errors import * | |
15 | ||
16 | class register(Command): | |
17 | ||
e55c4dd1 | 18 | description = ("register the distribution with the Python package index") |
1e4a197e RD |
19 | |
20 | DEFAULT_REPOSITORY = 'http://www.python.org/pypi' | |
21 | ||
22 | user_options = [ | |
23 | ('repository=', 'r', | |
24 | "url of repository [default: %s]"%DEFAULT_REPOSITORY), | |
1e4a197e RD |
25 | ('list-classifiers', None, |
26 | 'list the valid Trove classifiers'), | |
27 | ('show-response', None, | |
28 | 'display full response text from server'), | |
29 | ] | |
30 | boolean_options = ['verify', 'show-response', 'list-classifiers'] | |
31 | ||
32 | def initialize_options(self): | |
33 | self.repository = None | |
1e4a197e RD |
34 | self.show_response = 0 |
35 | 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() | |
e55c4dd1 | 43 | if self.dry_run: |
1e4a197e RD |
44 | 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 | |
141 | Your 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(), | |
232 | 'classifiers': meta.get_classifiers(), | |
233 | 'download_url': meta.get_download_url(), | |
234 | } | |
235 | 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: | |
277 | if self.show_response: | |
278 | data = e.fp.read() | |
279 | result = e.code, e.msg | |
280 | except urllib2.URLError, e: | |
281 | result = 500, str(e) | |
282 | else: | |
283 | if self.show_response: | |
284 | data = result.read() | |
285 | result = 200, 'OK' | |
286 | if self.show_response: | |
287 | print '-'*75, data, '-'*75 | |
288 | return result | |
289 |