]>
Commit | Line | Data |
---|---|---|
4710c53d | 1 | """distutils.command.register\r |
2 | \r | |
3 | Implements the Distutils 'register' command (register with the repository).\r | |
4 | """\r | |
5 | \r | |
6 | # created 2002/10/21, Richard Jones\r | |
7 | \r | |
8 | __revision__ = "$Id$"\r | |
9 | \r | |
10 | import urllib2\r | |
11 | import getpass\r | |
12 | import urlparse\r | |
13 | import StringIO\r | |
14 | from warnings import warn\r | |
15 | \r | |
16 | from distutils.core import PyPIRCCommand\r | |
17 | from distutils import log\r | |
18 | \r | |
19 | class register(PyPIRCCommand):\r | |
20 | \r | |
21 | description = ("register the distribution with the Python package index")\r | |
22 | user_options = PyPIRCCommand.user_options + [\r | |
23 | ('list-classifiers', None,\r | |
24 | 'list the valid Trove classifiers'),\r | |
25 | ('strict', None ,\r | |
26 | 'Will stop the registering if the meta-data are not fully compliant')\r | |
27 | ]\r | |
28 | boolean_options = PyPIRCCommand.boolean_options + [\r | |
29 | 'verify', 'list-classifiers', 'strict']\r | |
30 | \r | |
31 | sub_commands = [('check', lambda self: True)]\r | |
32 | \r | |
33 | def initialize_options(self):\r | |
34 | PyPIRCCommand.initialize_options(self)\r | |
35 | self.list_classifiers = 0\r | |
36 | self.strict = 0\r | |
37 | \r | |
38 | def finalize_options(self):\r | |
39 | PyPIRCCommand.finalize_options(self)\r | |
40 | # setting options for the `check` subcommand\r | |
41 | check_options = {'strict': ('register', self.strict),\r | |
42 | 'restructuredtext': ('register', 1)}\r | |
43 | self.distribution.command_options['check'] = check_options\r | |
44 | \r | |
45 | def run(self):\r | |
46 | self.finalize_options()\r | |
47 | self._set_config()\r | |
48 | \r | |
49 | # Run sub commands\r | |
50 | for cmd_name in self.get_sub_commands():\r | |
51 | self.run_command(cmd_name)\r | |
52 | \r | |
53 | if self.dry_run:\r | |
54 | self.verify_metadata()\r | |
55 | elif self.list_classifiers:\r | |
56 | self.classifiers()\r | |
57 | else:\r | |
58 | self.send_metadata()\r | |
59 | \r | |
60 | def check_metadata(self):\r | |
61 | """Deprecated API."""\r | |
62 | warn("distutils.command.register.check_metadata is deprecated, \\r | |
63 | use the check command instead", PendingDeprecationWarning)\r | |
64 | check = self.distribution.get_command_obj('check')\r | |
65 | check.ensure_finalized()\r | |
66 | check.strict = self.strict\r | |
67 | check.restructuredtext = 1\r | |
68 | check.run()\r | |
69 | \r | |
70 | def _set_config(self):\r | |
71 | ''' Reads the configuration file and set attributes.\r | |
72 | '''\r | |
73 | config = self._read_pypirc()\r | |
74 | if config != {}:\r | |
75 | self.username = config['username']\r | |
76 | self.password = config['password']\r | |
77 | self.repository = config['repository']\r | |
78 | self.realm = config['realm']\r | |
79 | self.has_config = True\r | |
80 | else:\r | |
81 | if self.repository not in ('pypi', self.DEFAULT_REPOSITORY):\r | |
82 | raise ValueError('%s not found in .pypirc' % self.repository)\r | |
83 | if self.repository == 'pypi':\r | |
84 | self.repository = self.DEFAULT_REPOSITORY\r | |
85 | self.has_config = False\r | |
86 | \r | |
87 | def classifiers(self):\r | |
88 | ''' Fetch the list of classifiers from the server.\r | |
89 | '''\r | |
90 | response = urllib2.urlopen(self.repository+'?:action=list_classifiers')\r | |
91 | log.info(response.read())\r | |
92 | \r | |
93 | def verify_metadata(self):\r | |
94 | ''' Send the metadata to the package index server to be checked.\r | |
95 | '''\r | |
96 | # send the info to the server and report the result\r | |
97 | (code, result) = self.post_to_server(self.build_post_data('verify'))\r | |
98 | log.info('Server response (%s): %s' % (code, result))\r | |
99 | \r | |
100 | \r | |
101 | def send_metadata(self):\r | |
102 | ''' Send the metadata to the package index server.\r | |
103 | \r | |
104 | Well, do the following:\r | |
105 | 1. figure who the user is, and then\r | |
106 | 2. send the data as a Basic auth'ed POST.\r | |
107 | \r | |
108 | First we try to read the username/password from $HOME/.pypirc,\r | |
109 | which is a ConfigParser-formatted file with a section\r | |
110 | [distutils] containing username and password entries (both\r | |
111 | in clear text). Eg:\r | |
112 | \r | |
113 | [distutils]\r | |
114 | index-servers =\r | |
115 | pypi\r | |
116 | \r | |
117 | [pypi]\r | |
118 | username: fred\r | |
119 | password: sekrit\r | |
120 | \r | |
121 | Otherwise, to figure who the user is, we offer the user three\r | |
122 | choices:\r | |
123 | \r | |
124 | 1. use existing login,\r | |
125 | 2. register as a new user, or\r | |
126 | 3. set the password to a random string and email the user.\r | |
127 | \r | |
128 | '''\r | |
129 | # see if we can short-cut and get the username/password from the\r | |
130 | # config\r | |
131 | if self.has_config:\r | |
132 | choice = '1'\r | |
133 | username = self.username\r | |
134 | password = self.password\r | |
135 | else:\r | |
136 | choice = 'x'\r | |
137 | username = password = ''\r | |
138 | \r | |
139 | # get the user's login info\r | |
140 | choices = '1 2 3 4'.split()\r | |
141 | while choice not in choices:\r | |
142 | self.announce('''\\r | |
143 | We need to know who you are, so please choose either:\r | |
144 | 1. use your existing login,\r | |
145 | 2. register as a new user,\r | |
146 | 3. have the server generate a new password for you (and email it to you), or\r | |
147 | 4. quit\r | |
148 | Your selection [default 1]: ''', log.INFO)\r | |
149 | \r | |
150 | choice = raw_input()\r | |
151 | if not choice:\r | |
152 | choice = '1'\r | |
153 | elif choice not in choices:\r | |
154 | print 'Please choose one of the four options!'\r | |
155 | \r | |
156 | if choice == '1':\r | |
157 | # get the username and password\r | |
158 | while not username:\r | |
159 | username = raw_input('Username: ')\r | |
160 | while not password:\r | |
161 | password = getpass.getpass('Password: ')\r | |
162 | \r | |
163 | # set up the authentication\r | |
164 | auth = urllib2.HTTPPasswordMgr()\r | |
165 | host = urlparse.urlparse(self.repository)[1]\r | |
166 | auth.add_password(self.realm, host, username, password)\r | |
167 | # send the info to the server and report the result\r | |
168 | code, result = self.post_to_server(self.build_post_data('submit'),\r | |
169 | auth)\r | |
170 | self.announce('Server response (%s): %s' % (code, result),\r | |
171 | log.INFO)\r | |
172 | \r | |
173 | # possibly save the login\r | |
174 | if code == 200:\r | |
175 | if self.has_config:\r | |
176 | # sharing the password in the distribution instance\r | |
177 | # so the upload command can reuse it\r | |
178 | self.distribution.password = password\r | |
179 | else:\r | |
180 | self.announce(('I can store your PyPI login so future '\r | |
181 | 'submissions will be faster.'), log.INFO)\r | |
182 | self.announce('(the login will be stored in %s)' % \\r | |
183 | self._get_rc_file(), log.INFO)\r | |
184 | choice = 'X'\r | |
185 | while choice.lower() not in 'yn':\r | |
186 | choice = raw_input('Save your login (y/N)?')\r | |
187 | if not choice:\r | |
188 | choice = 'n'\r | |
189 | if choice.lower() == 'y':\r | |
190 | self._store_pypirc(username, password)\r | |
191 | \r | |
192 | elif choice == '2':\r | |
193 | data = {':action': 'user'}\r | |
194 | data['name'] = data['password'] = data['email'] = ''\r | |
195 | data['confirm'] = None\r | |
196 | while not data['name']:\r | |
197 | data['name'] = raw_input('Username: ')\r | |
198 | while data['password'] != data['confirm']:\r | |
199 | while not data['password']:\r | |
200 | data['password'] = getpass.getpass('Password: ')\r | |
201 | while not data['confirm']:\r | |
202 | data['confirm'] = getpass.getpass(' Confirm: ')\r | |
203 | if data['password'] != data['confirm']:\r | |
204 | data['password'] = ''\r | |
205 | data['confirm'] = None\r | |
206 | print "Password and confirm don't match!"\r | |
207 | while not data['email']:\r | |
208 | data['email'] = raw_input(' EMail: ')\r | |
209 | code, result = self.post_to_server(data)\r | |
210 | if code != 200:\r | |
211 | log.info('Server response (%s): %s' % (code, result))\r | |
212 | else:\r | |
213 | log.info('You will receive an email shortly.')\r | |
214 | log.info(('Follow the instructions in it to '\r | |
215 | 'complete registration.'))\r | |
216 | elif choice == '3':\r | |
217 | data = {':action': 'password_reset'}\r | |
218 | data['email'] = ''\r | |
219 | while not data['email']:\r | |
220 | data['email'] = raw_input('Your email address: ')\r | |
221 | code, result = self.post_to_server(data)\r | |
222 | log.info('Server response (%s): %s' % (code, result))\r | |
223 | \r | |
224 | def build_post_data(self, action):\r | |
225 | # figure the data to send - the metadata plus some additional\r | |
226 | # information used by the package server\r | |
227 | meta = self.distribution.metadata\r | |
228 | data = {\r | |
229 | ':action': action,\r | |
230 | 'metadata_version' : '1.0',\r | |
231 | 'name': meta.get_name(),\r | |
232 | 'version': meta.get_version(),\r | |
233 | 'summary': meta.get_description(),\r | |
234 | 'home_page': meta.get_url(),\r | |
235 | 'author': meta.get_contact(),\r | |
236 | 'author_email': meta.get_contact_email(),\r | |
237 | 'license': meta.get_licence(),\r | |
238 | 'description': meta.get_long_description(),\r | |
239 | 'keywords': meta.get_keywords(),\r | |
240 | 'platform': meta.get_platforms(),\r | |
241 | 'classifiers': meta.get_classifiers(),\r | |
242 | 'download_url': meta.get_download_url(),\r | |
243 | # PEP 314\r | |
244 | 'provides': meta.get_provides(),\r | |
245 | 'requires': meta.get_requires(),\r | |
246 | 'obsoletes': meta.get_obsoletes(),\r | |
247 | }\r | |
248 | if data['provides'] or data['requires'] or data['obsoletes']:\r | |
249 | data['metadata_version'] = '1.1'\r | |
250 | return data\r | |
251 | \r | |
252 | def post_to_server(self, data, auth=None):\r | |
253 | ''' Post a query to the server, and return a string response.\r | |
254 | '''\r | |
255 | if 'name' in data:\r | |
256 | self.announce('Registering %s to %s' % (data['name'],\r | |
257 | self.repository),\r | |
258 | log.INFO)\r | |
259 | # Build up the MIME payload for the urllib2 POST data\r | |
260 | boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254'\r | |
261 | sep_boundary = '\n--' + boundary\r | |
262 | end_boundary = sep_boundary + '--'\r | |
263 | body = StringIO.StringIO()\r | |
264 | for key, value in data.items():\r | |
265 | # handle multiple entries for the same name\r | |
266 | if type(value) not in (type([]), type( () )):\r | |
267 | value = [value]\r | |
268 | for value in value:\r | |
269 | body.write(sep_boundary)\r | |
270 | body.write('\nContent-Disposition: form-data; name="%s"'%key)\r | |
271 | body.write("\n\n")\r | |
272 | body.write(value)\r | |
273 | if value and value[-1] == '\r':\r | |
274 | body.write('\n') # write an extra newline (lurve Macs)\r | |
275 | body.write(end_boundary)\r | |
276 | body.write("\n")\r | |
277 | body = body.getvalue()\r | |
278 | \r | |
279 | # build the Request\r | |
280 | headers = {\r | |
281 | 'Content-type': 'multipart/form-data; boundary=%s; charset=utf-8'%boundary,\r | |
282 | 'Content-length': str(len(body))\r | |
283 | }\r | |
284 | req = urllib2.Request(self.repository, body, headers)\r | |
285 | \r | |
286 | # handle HTTP and include the Basic Auth handler\r | |
287 | opener = urllib2.build_opener(\r | |
288 | urllib2.HTTPBasicAuthHandler(password_mgr=auth)\r | |
289 | )\r | |
290 | data = ''\r | |
291 | try:\r | |
292 | result = opener.open(req)\r | |
293 | except urllib2.HTTPError, e:\r | |
294 | if self.show_response:\r | |
295 | data = e.fp.read()\r | |
296 | result = e.code, e.msg\r | |
297 | except urllib2.URLError, e:\r | |
298 | result = 500, str(e)\r | |
299 | else:\r | |
300 | if self.show_response:\r | |
301 | data = result.read()\r | |
302 | result = 200, 'OK'\r | |
303 | if self.show_response:\r | |
304 | dashes = '-' * 75\r | |
305 | self.announce('%s%s%s' % (dashes, data, dashes))\r | |
306 | \r | |
307 | return result\r |