Merge lp://staging/~gary/launchpadlib/buildout into lp://staging/~launchpad-pqm/launchpadlib/devel
- buildout
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Francis J. Lacoste |
Approved revision: | 39 |
Merged at revision: | not available |
Proposed branch: | lp://staging/~gary/launchpadlib/buildout |
Merge into: | lp://staging/~launchpad-pqm/launchpadlib/devel |
Diff against target: | None lines |
To merge this branch: | bzr merge lp://staging/~gary/launchpadlib/buildout |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Francis J. Lacoste (community) | Approve | ||
LAZR Developers | Pending | ||
launchpadlib developers | Pending | ||
Review via email:
|
Commit message
Description of the change
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Gary Poster (gary) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Francis J. Lacoste (flacoste) wrote : | # |
On March 19, 2009, Gary Poster wrote:
> Gary Poster has proposed merging lp:~gary/launchpadlib/buildout into
> lp:launchpadlib.
>
> Requested reviews:
> LAZR Developers (lazr-developers)
> Francis J. Lacoste (flacoste)
> launchpadlib developers (launchpadlib-
>
> This makes launchpadlib built with buildout.
>
> There are no tests or documents in the package, so this package looks more
> bare than usual.
>
> This requires lazr.uri and the wadllib package that I also have up for
> review. See https:/
> by checking them out, ``cd``'ing into the directory, running ``python
> bootstrap.py``, running ``./bin/buildout``, and running ``./bin/buildout
> setup . sdist``. Then put the .tgz file in the download-cache/dist
> directory.
>
Hi Gary,
Again, make sure that all 'or later' clause are removed. I have a few other
small comments. But this is ready to move to PyPI!
status approved
review approve
> === modified file 'TODO.txt'
> --- TODO.txt 2008-08-01 16:46:08 +0000
> +++ TODO.txt 2009-03-16 21:02:52 +0000
> @@ -4,5 +4,4 @@
> - make == and is work
> - integrate httplib2 caching; conditional get; conditional patch
That last one has been done.
> - Flesh out HTTPErrors as per Francis email
> -- Move launchpadlib.
> - Support hosted file resources
So has that one.
Actually, I think we should simply get rid of that file. We have a bug tracker
for these things.
> --- src/launchpadli
> +++ src/launchpadli
> @@ -0,0 +1,8 @@
> +======
> +NEWS for launchpadlib
> +======
> +
> +1.0 (200X-XX-XX)
> +================
> +
> +- Initial release
Should be updated with 'First PyPI release.'
--
Francis J. Lacoste
<email address hidden>
- 40. By Gary Poster
-
v3 of LGPL only. settle on this release being 1.0, not 0.2
- 41. By Gary Poster
-
acknowledge reality in terms of release dates
- 42. By Gary Poster
-
adjust docs: we are claiming the library to be 1.0 rather than 0.2
Preview Diff
1 | === modified file '.bzrignore' |
2 | --- .bzrignore 2009-02-04 07:30:54 +0000 |
3 | +++ .bzrignore 2009-03-19 15:22:56 +0000 |
4 | @@ -1,4 +1,11 @@ |
5 | +bin |
6 | +develop-eggs |
7 | +.installed.cfg |
8 | +develop-eggs |
9 | +parts |
10 | +*.egg-info |
11 | +tags |
12 | +TAGS |
13 | build |
14 | +*.egg |
15 | dist |
16 | -launchpadlib.egg-info |
17 | -setuptools_bzr-1.2-py2.5.egg |
18 | |
19 | === added file 'HACKING.txt' |
20 | --- HACKING.txt 1970-01-01 00:00:00 +0000 |
21 | +++ HACKING.txt 2009-03-16 21:02:52 +0000 |
22 | @@ -0,0 +1,42 @@ |
23 | +.. |
24 | + This file is part of lazr.launchpadlib. |
25 | + |
26 | + lazr.launchpadlib is free software: you can redistribute it and/or modify it |
27 | + under the terms of the GNU Lesser General Public License as published by |
28 | + the Free Software Foundation, either version 3 of the License, or (at your |
29 | + option) any later version. |
30 | + |
31 | + lazr.launchpadlib is distributed in the hope that it will be useful, but WITHOUT |
32 | + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
33 | + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public |
34 | + License for more details. |
35 | + |
36 | + You should have received a copy of the GNU Lesser General Public License |
37 | + along with lazr.launchpadlib. If not, see <http://www.gnu.org/licenses/>. |
38 | + |
39 | +This project uses zc.buildout for development. |
40 | + |
41 | +============ |
42 | +Introduction |
43 | +============ |
44 | + |
45 | +These are guidelines for hacking on the lazr.launchpadlib project. But first, |
46 | +please see the common hacking guidelines at: |
47 | + |
48 | + http://dev.launchpad.net/Hacking |
49 | + |
50 | + |
51 | +Getting help |
52 | +------------ |
53 | + |
54 | +If you find bugs in this package, you can report them here: |
55 | + |
56 | + https://launchpad.net/lazr.launchpadlib |
57 | + |
58 | +If you want to discuss this package, join the team and mailing list here: |
59 | + |
60 | + https://launchpad.net/~lazr-developers |
61 | + |
62 | +or send a message to: |
63 | + |
64 | + lazr-developers@lists.launchpad.net |
65 | |
66 | === modified file 'README.txt' |
67 | --- README.txt 2008-08-01 16:05:31 +0000 |
68 | +++ README.txt 2009-03-16 21:02:52 +0000 |
69 | @@ -1,37 +1,38 @@ |
70 | -Copyright (C) 2008 Canonical Ltd. |
71 | - |
72 | -This file is part of launchpadlib. |
73 | - |
74 | -launchpadlib is free software: you can redistribute it and/or modify it |
75 | -under the terms of the GNU Lesser General Public License as published |
76 | -by the Free Software Foundation, either version 3 of the License, or |
77 | -(at your option) any later version. |
78 | - |
79 | -launchpadlib is distributed in the hope that it will be useful, but WITHOUT |
80 | -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
81 | -FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public |
82 | -License for more details. |
83 | - |
84 | -You should have received a copy of the GNU Lesser General Public |
85 | -License along with launchpadlib. If not, see |
86 | -<http://www.gnu.org/licenses/>. |
87 | - |
88 | - |
89 | -== Overview == |
90 | +Script Launchpad through its web services interfaces. Officially supported. |
91 | + |
92 | +.. |
93 | + Copyright (C) 2008-2009 Canonical Ltd. |
94 | + |
95 | + This file is part of launchpadlib. |
96 | + |
97 | + launchpadlib is free software: you can redistribute it and/or modify it |
98 | + under the terms of the GNU Lesser General Public License as published by |
99 | + the Free Software Foundation, either version 3 of the License, or (at your |
100 | + option) any later version. |
101 | + |
102 | + launchpadlib is distributed in the hope that it will be useful, but WITHOUT |
103 | + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
104 | + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public |
105 | + License for more details. |
106 | + |
107 | + You should have received a copy of the GNU Lesser General Public License |
108 | + along with launchpadlib. If not, see <http://www.gnu.org/licenses/>. |
109 | + |
110 | + |
111 | +Overview |
112 | +******** |
113 | |
114 | launchpadlib is a standalone Python library for scripting Launchpad through |
115 | its web services interface. It is the officially supported bindings to the |
116 | Launchpad web service, but there may be third party bindings that provide |
117 | scriptability for other languages. |
118 | |
119 | -For information on Launchpad itself, see |
120 | +Launchpad (http://launchpad.net) is a a free software hosting and development |
121 | +website, making it easy to collaborate across multiple projects. For |
122 | +information on Launchpad itself, see |
123 | |
124 | https://help.launchpad.net |
125 | |
126 | -Launchpad is available at |
127 | - |
128 | - https://launchpad.net |
129 | - |
130 | More information on the Launchpad web service, such as user guides and |
131 | reference documentation, are available at |
132 | |
133 | |
134 | === modified file 'TODO.txt' |
135 | --- TODO.txt 2008-08-01 16:46:08 +0000 |
136 | +++ TODO.txt 2009-03-16 21:02:52 +0000 |
137 | @@ -4,5 +4,4 @@ |
138 | - make == and is work |
139 | - integrate httplib2 caching; conditional get; conditional patch |
140 | - Flesh out HTTPErrors as per Francis email |
141 | -- Move launchpadlib._utils.uri into a separate cheeseshop package |
142 | - Support hosted file resources |
143 | |
144 | === added file 'bootstrap.py' |
145 | --- bootstrap.py 1970-01-01 00:00:00 +0000 |
146 | +++ bootstrap.py 2009-03-16 21:02:52 +0000 |
147 | @@ -0,0 +1,77 @@ |
148 | +############################################################################## |
149 | +# |
150 | +# Copyright (c) 2006 Zope Corporation and Contributors. |
151 | +# All Rights Reserved. |
152 | +# |
153 | +# This software is subject to the provisions of the Zope Public License, |
154 | +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. |
155 | +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED |
156 | +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
157 | +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS |
158 | +# FOR A PARTICULAR PURPOSE. |
159 | +# |
160 | +############################################################################## |
161 | +"""Bootstrap a buildout-based project |
162 | + |
163 | +Simply run this script in a directory containing a buildout.cfg. |
164 | +The script accepts buildout command-line options, so you can |
165 | +use the -c option to specify an alternate configuration file. |
166 | + |
167 | +$Id$ |
168 | +""" |
169 | + |
170 | +import os, shutil, sys, tempfile, urllib2 |
171 | + |
172 | +tmpeggs = tempfile.mkdtemp() |
173 | + |
174 | +is_jython = sys.platform.startswith('java') |
175 | + |
176 | +try: |
177 | + import pkg_resources |
178 | +except ImportError: |
179 | + ez = {} |
180 | + exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py' |
181 | + ).read() in ez |
182 | + ez['use_setuptools'](to_dir=tmpeggs, download_delay=0) |
183 | + |
184 | + import pkg_resources |
185 | + |
186 | +if sys.platform == 'win32': |
187 | + def quote(c): |
188 | + if ' ' in c: |
189 | + return '"%s"' % c # work around spawn lamosity on windows |
190 | + else: |
191 | + return c |
192 | +else: |
193 | + def quote (c): |
194 | + return c |
195 | + |
196 | +cmd = 'from setuptools.command.easy_install import main; main()' |
197 | +ws = pkg_resources.working_set |
198 | + |
199 | +if is_jython: |
200 | + import subprocess |
201 | + |
202 | + assert subprocess.Popen([sys.executable] + ['-c', quote(cmd), '-mqNxd', |
203 | + quote(tmpeggs), 'zc.buildout'], |
204 | + env=dict(os.environ, |
205 | + PYTHONPATH= |
206 | + ws.find(pkg_resources.Requirement.parse('setuptools')).location |
207 | + ), |
208 | + ).wait() == 0 |
209 | + |
210 | +else: |
211 | + assert os.spawnle( |
212 | + os.P_WAIT, sys.executable, quote (sys.executable), |
213 | + '-c', quote (cmd), '-mqNxd', quote (tmpeggs), 'zc.buildout', |
214 | + dict(os.environ, |
215 | + PYTHONPATH= |
216 | + ws.find(pkg_resources.Requirement.parse('setuptools')).location |
217 | + ), |
218 | + ) == 0 |
219 | + |
220 | +ws.add_entry(tmpeggs) |
221 | +ws.require('zc.buildout') |
222 | +import zc.buildout.buildout |
223 | +zc.buildout.buildout.main(sys.argv[1:] + ['bootstrap']) |
224 | +shutil.rmtree(tmpeggs) |
225 | |
226 | === added file 'buildout.cfg' |
227 | --- buildout.cfg 1970-01-01 00:00:00 +0000 |
228 | +++ buildout.cfg 2009-03-19 15:22:56 +0000 |
229 | @@ -0,0 +1,31 @@ |
230 | +[buildout] |
231 | +parts = |
232 | + interpreter |
233 | +# test |
234 | +# docs |
235 | + tags |
236 | +unzip = true |
237 | + |
238 | +develop = . |
239 | + |
240 | +[test] |
241 | +recipe = zc.recipe.testrunner |
242 | +eggs = launchpadlib |
243 | +defaults = '--tests-pattern ^tests --exit-with-status --suite-name additional_tests'.split() |
244 | + |
245 | +[docs] |
246 | +recipe = z3c.recipe.sphinxdoc |
247 | +eggs = launchpadlib [docs] |
248 | +index-doc = README |
249 | +default.css = |
250 | +layout.html = |
251 | + |
252 | +[interpreter] |
253 | +recipe = zc.recipe.egg |
254 | +interpreter = py |
255 | +eggs = launchpadlib |
256 | + docutils |
257 | + |
258 | +[tags] |
259 | +recipe = z3c.recipe.tag:tags |
260 | +eggs = launchpadlib |
261 | |
262 | === removed directory 'launchpadlib/_oauth' |
263 | === removed file 'launchpadlib/_oauth/__init__.py' |
264 | === removed file 'launchpadlib/_oauth/oauth.py' |
265 | --- launchpadlib/_oauth/oauth.py 2008-08-01 15:45:31 +0000 |
266 | +++ launchpadlib/_oauth/oauth.py 1970-01-01 00:00:00 +0000 |
267 | @@ -1,525 +0,0 @@ |
268 | -import cgi |
269 | -import urllib |
270 | -import time |
271 | -import random |
272 | -import urlparse |
273 | -import hmac |
274 | -import base64 |
275 | - |
276 | -VERSION = '1.0' # Hi Blaine! |
277 | -HTTP_METHOD = 'GET' |
278 | -SIGNATURE_METHOD = 'PLAINTEXT' |
279 | - |
280 | -# Generic exception class |
281 | -class OAuthError(RuntimeError): |
282 | - def __init__(self, message='OAuth error occured.'): |
283 | - self.message = message |
284 | - |
285 | -# optional WWW-Authenticate header (401 error) |
286 | -def build_authenticate_header(realm=''): |
287 | - return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} |
288 | - |
289 | -# url escape |
290 | -def escape(s): |
291 | - # escape '/' too |
292 | - return urllib.quote(s, safe='~') |
293 | - |
294 | -# util function: current timestamp |
295 | -# seconds since epoch (UTC) |
296 | -def generate_timestamp(): |
297 | - return int(time.time()) |
298 | - |
299 | -# util function: nonce |
300 | -# pseudorandom number |
301 | -def generate_nonce(length=8): |
302 | - return ''.join(str(random.randint(0, 9)) for i in range(length)) |
303 | - |
304 | -# OAuthConsumer is a data type that represents the identity of the Consumer |
305 | -# via its shared secret with the Service Provider. |
306 | -class OAuthConsumer(object): |
307 | - key = None |
308 | - secret = None |
309 | - |
310 | - def __init__(self, key, secret): |
311 | - self.key = key |
312 | - self.secret = secret |
313 | - |
314 | -# OAuthToken is a data type that represents an End User via either an access |
315 | -# or request token. |
316 | -class OAuthToken(object): |
317 | - # access tokens and request tokens |
318 | - key = None |
319 | - secret = None |
320 | - |
321 | - ''' |
322 | - key = the token |
323 | - secret = the token secret |
324 | - ''' |
325 | - def __init__(self, key, secret): |
326 | - self.key = key |
327 | - self.secret = secret |
328 | - |
329 | - def to_string(self): |
330 | - return urllib.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret}) |
331 | - |
332 | - # return a token from something like: |
333 | - # oauth_token_secret=digg&oauth_token=digg |
334 | - @staticmethod |
335 | - def from_string(s): |
336 | - params = cgi.parse_qs(s, keep_blank_values=False) |
337 | - key = params['oauth_token'][0] |
338 | - secret = params['oauth_token_secret'][0] |
339 | - return OAuthToken(key, secret) |
340 | - |
341 | - def __str__(self): |
342 | - return self.to_string() |
343 | - |
344 | -# OAuthRequest represents the request and can be serialized |
345 | -class OAuthRequest(object): |
346 | - ''' |
347 | - OAuth parameters: |
348 | - - oauth_consumer_key |
349 | - - oauth_token |
350 | - - oauth_signature_method |
351 | - - oauth_signature |
352 | - - oauth_timestamp |
353 | - - oauth_nonce |
354 | - - oauth_version |
355 | - ... any additional parameters, as defined by the Service Provider. |
356 | - ''' |
357 | - parameters = None # oauth parameters |
358 | - http_method = HTTP_METHOD |
359 | - http_url = None |
360 | - version = VERSION |
361 | - |
362 | - def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None): |
363 | - self.http_method = http_method |
364 | - self.http_url = http_url |
365 | - self.parameters = parameters or {} |
366 | - |
367 | - def set_parameter(self, parameter, value): |
368 | - self.parameters[parameter] = value |
369 | - |
370 | - def get_parameter(self, parameter): |
371 | - try: |
372 | - return self.parameters[parameter] |
373 | - except: |
374 | - raise OAuthError('Parameter not found: %s' % parameter) |
375 | - |
376 | - def _get_timestamp_nonce(self): |
377 | - return self.get_parameter('oauth_timestamp'), self.get_parameter('oauth_nonce') |
378 | - |
379 | - # get any non-oauth parameters |
380 | - def get_nonoauth_parameters(self): |
381 | - parameters = {} |
382 | - for k, v in self.parameters.iteritems(): |
383 | - # ignore oauth parameters |
384 | - if k.find('oauth_') < 0: |
385 | - parameters[k] = v |
386 | - return parameters |
387 | - |
388 | - # serialize as a header for an HTTPAuth request |
389 | - def to_header(self, realm=''): |
390 | - auth_header = 'OAuth realm="%s"' % realm |
391 | - # add the oauth parameters |
392 | - if self.parameters: |
393 | - for k, v in self.parameters.iteritems(): |
394 | - auth_header += ', %s="%s"' % (k, v) |
395 | - return {'Authorization': auth_header} |
396 | - |
397 | - # serialize as post data for a POST request |
398 | - def to_postdata(self): |
399 | - return '&'.join('%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.iteritems()) |
400 | - |
401 | - # serialize as a url for a GET request |
402 | - def to_url(self): |
403 | - return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata()) |
404 | - |
405 | - # return a string that consists of all the parameters that need to be signed |
406 | - def get_normalized_parameters(self): |
407 | - params = self.parameters |
408 | - try: |
409 | - # exclude the signature if it exists |
410 | - del params['oauth_signature'] |
411 | - except: |
412 | - pass |
413 | - key_values = params.items() |
414 | - # sort lexicographically, first after key, then after value |
415 | - key_values.sort() |
416 | - # combine key value pairs in string and escape |
417 | - return '&'.join('%s=%s' % (escape(str(k)), escape(str(v))) for k, v in key_values) |
418 | - |
419 | - # just uppercases the http method |
420 | - def get_normalized_http_method(self): |
421 | - return self.http_method.upper() |
422 | - |
423 | - # parses the url and rebuilds it to be scheme://host/path |
424 | - def get_normalized_http_url(self): |
425 | - parts = urlparse.urlparse(self.http_url) |
426 | - url_string = '%s://%s%s' % (parts[0], parts[1], parts[2]) # scheme, netloc, path |
427 | - return url_string |
428 | - |
429 | - # set the signature parameter to the result of build_signature |
430 | - def sign_request(self, signature_method, consumer, token): |
431 | - # set the signature method |
432 | - self.set_parameter('oauth_signature_method', signature_method.get_name()) |
433 | - # set the signature |
434 | - self.set_parameter('oauth_signature', self.build_signature(signature_method, consumer, token)) |
435 | - |
436 | - def build_signature(self, signature_method, consumer, token): |
437 | - # call the build signature method within the signature method |
438 | - return signature_method.build_signature(self, consumer, token) |
439 | - |
440 | - @staticmethod |
441 | - def from_request(http_method, http_url, headers=None, postdata=None, parameters=None): |
442 | - |
443 | - # let the library user override things however they'd like, if they know |
444 | - # which parameters to use then go for it, for example XMLRPC might want to |
445 | - # do this |
446 | - if parameters is not None: |
447 | - return OAuthRequest(http_method, http_url, parameters) |
448 | - |
449 | - # from the headers |
450 | - if headers and 'Authorization' in headers: |
451 | - try: |
452 | - auth_header = headers['Authorization'] |
453 | - # check that the authorization header is OAuth |
454 | - auth_header.index('OAuth') |
455 | - # get the parameters from the header |
456 | - parameters = OAuthRequest._split_header(auth_header) |
457 | - return OAuthRequest(http_method, http_url, parameters) |
458 | - except: |
459 | - raise OAuthError('Unable to parse OAuth parameters from Authorization header.') |
460 | - |
461 | - # from the parameter string (post body) |
462 | - if http_method == 'POST' and postdata is not None: |
463 | - parameters = OAuthRequest._split_url_string(postdata) |
464 | - |
465 | - # from the url string |
466 | - elif http_method == 'GET': |
467 | - param_str = urlparse.urlparse(http_url).query |
468 | - parameters = OAuthRequest._split_url_string(param_str) |
469 | - |
470 | - if parameters: |
471 | - return OAuthRequest(http_method, http_url, parameters) |
472 | - |
473 | - raise OAuthError('Missing all OAuth parameters. OAuth parameters must be in the headers, post body, or url.') |
474 | - |
475 | - @staticmethod |
476 | - def from_consumer_and_token(oauth_consumer, token=None, http_method=HTTP_METHOD, http_url=None, parameters=None): |
477 | - if not parameters: |
478 | - parameters = {} |
479 | - |
480 | - defaults = { |
481 | - 'oauth_consumer_key': oauth_consumer.key, |
482 | - 'oauth_timestamp': generate_timestamp(), |
483 | - 'oauth_nonce': generate_nonce(), |
484 | - 'oauth_version': OAuthRequest.version, |
485 | - } |
486 | - |
487 | - defaults.update(parameters) |
488 | - parameters = defaults |
489 | - |
490 | - if token: |
491 | - parameters['oauth_token'] = token.key |
492 | - |
493 | - return OAuthRequest(http_method, http_url, parameters) |
494 | - |
495 | - @staticmethod |
496 | - def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD, http_url=None, parameters=None): |
497 | - if not parameters: |
498 | - parameters = {} |
499 | - |
500 | - parameters['oauth_token'] = token.key |
501 | - |
502 | - if callback: |
503 | - parameters['oauth_callback'] = escape(callback) |
504 | - |
505 | - return OAuthRequest(http_method, http_url, parameters) |
506 | - |
507 | - # util function: turn Authorization: header into parameters, has to do some unescaping |
508 | - @staticmethod |
509 | - def _split_header(header): |
510 | - params = {} |
511 | - parts = header.split(',') |
512 | - for param in parts: |
513 | - # ignore realm parameter |
514 | - if param.find('OAuth realm') > -1: |
515 | - continue |
516 | - # remove whitespace |
517 | - param = param.strip() |
518 | - # split key-value |
519 | - param_parts = param.split('=', 1) |
520 | - # remove quotes and unescape the value |
521 | - params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"')) |
522 | - return params |
523 | - |
524 | - # util function: turn url string into parameters, has to do some unescaping |
525 | - @staticmethod |
526 | - def _split_url_string(param_str): |
527 | - parameters = cgi.parse_qs(param_str, keep_blank_values=False) |
528 | - for k, v in parameters.iteritems(): |
529 | - parameters[k] = urllib.unquote(v[0]) |
530 | - return parameters |
531 | - |
532 | -# OAuthServer is a worker to check a requests validity against a data store |
533 | -class OAuthServer(object): |
534 | - timestamp_threshold = 300 # in seconds, five minutes |
535 | - version = VERSION |
536 | - signature_methods = None |
537 | - data_store = None |
538 | - |
539 | - def __init__(self, data_store=None, signature_methods=None): |
540 | - self.data_store = data_store |
541 | - self.signature_methods = signature_methods or {} |
542 | - |
543 | - def set_data_store(self, oauth_data_store): |
544 | - self.data_store = data_store |
545 | - |
546 | - def get_data_store(self): |
547 | - return self.data_store |
548 | - |
549 | - def add_signature_method(self, signature_method): |
550 | - self.signature_methods[signature_method.get_name()] = signature_method |
551 | - return self.signature_methods |
552 | - |
553 | - # process a request_token request |
554 | - # returns the request token on success |
555 | - def fetch_request_token(self, oauth_request): |
556 | - try: |
557 | - # get the request token for authorization |
558 | - token = self._get_token(oauth_request, 'request') |
559 | - except: |
560 | - # no token required for the initial token request |
561 | - version = self._get_version(oauth_request) |
562 | - consumer = self._get_consumer(oauth_request) |
563 | - self._check_signature(oauth_request, consumer, None) |
564 | - # fetch a new token |
565 | - token = self.data_store.fetch_request_token(consumer) |
566 | - return token |
567 | - |
568 | - # process an access_token request |
569 | - # returns the access token on success |
570 | - def fetch_access_token(self, oauth_request): |
571 | - version = self._get_version(oauth_request) |
572 | - consumer = self._get_consumer(oauth_request) |
573 | - # get the request token |
574 | - token = self._get_token(oauth_request, 'request') |
575 | - self._check_signature(oauth_request, consumer, token) |
576 | - new_token = self.data_store.fetch_access_token(consumer, token) |
577 | - return new_token |
578 | - |
579 | - # verify an api call, checks all the parameters |
580 | - def verify_request(self, oauth_request): |
581 | - # -> consumer and token |
582 | - version = self._get_version(oauth_request) |
583 | - consumer = self._get_consumer(oauth_request) |
584 | - # get the access token |
585 | - token = self._get_token(oauth_request, 'access') |
586 | - self._check_signature(oauth_request, consumer, token) |
587 | - parameters = oauth_request.get_nonoauth_parameters() |
588 | - return consumer, token, parameters |
589 | - |
590 | - # authorize a request token |
591 | - def authorize_token(self, token, user): |
592 | - return self.data_store.authorize_request_token(token, user) |
593 | - |
594 | - # get the callback url |
595 | - def get_callback(self, oauth_request): |
596 | - return oauth_request.get_parameter('oauth_callback') |
597 | - |
598 | - # optional support for the authenticate header |
599 | - def build_authenticate_header(self, realm=''): |
600 | - return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} |
601 | - |
602 | - # verify the correct version request for this server |
603 | - def _get_version(self, oauth_request): |
604 | - try: |
605 | - version = oauth_request.get_parameter('oauth_version') |
606 | - except: |
607 | - version = VERSION |
608 | - if version and version != self.version: |
609 | - raise OAuthError('OAuth version %s not supported.' % str(version)) |
610 | - return version |
611 | - |
612 | - # figure out the signature with some defaults |
613 | - def _get_signature_method(self, oauth_request): |
614 | - try: |
615 | - signature_method = oauth_request.get_parameter('oauth_signature_method') |
616 | - except: |
617 | - signature_method = SIGNATURE_METHOD |
618 | - try: |
619 | - # get the signature method object |
620 | - signature_method = self.signature_methods[signature_method] |
621 | - except: |
622 | - signature_method_names = ', '.join(self.signature_methods.keys()) |
623 | - raise OAuthError('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names)) |
624 | - |
625 | - return signature_method |
626 | - |
627 | - def _get_consumer(self, oauth_request): |
628 | - consumer_key = oauth_request.get_parameter('oauth_consumer_key') |
629 | - if not consumer_key: |
630 | - raise OAuthError('Invalid consumer key.') |
631 | - consumer = self.data_store.lookup_consumer(consumer_key) |
632 | - if not consumer: |
633 | - raise OAuthError('Invalid consumer.') |
634 | - return consumer |
635 | - |
636 | - # try to find the token for the provided request token key |
637 | - def _get_token(self, oauth_request, token_type='access'): |
638 | - token_field = oauth_request.get_parameter('oauth_token') |
639 | - token = self.data_store.lookup_token(token_type, token_field) |
640 | - if not token: |
641 | - raise OAuthError('Invalid %s token: %s' % (token_type, token_field)) |
642 | - return token |
643 | - |
644 | - def _check_signature(self, oauth_request, consumer, token): |
645 | - timestamp, nonce = oauth_request._get_timestamp_nonce() |
646 | - self._check_timestamp(timestamp) |
647 | - self._check_nonce(consumer, token, nonce) |
648 | - signature_method = self._get_signature_method(oauth_request) |
649 | - try: |
650 | - signature = oauth_request.get_parameter('oauth_signature') |
651 | - except: |
652 | - raise OAuthError('Missing signature.') |
653 | - # validate the signature |
654 | - valid_sig = signature_method.check_signature(oauth_request, consumer, token, signature) |
655 | - if not valid_sig: |
656 | - key, base = signature_method.build_signature_base_string(oauth_request, consumer, token) |
657 | - raise OAuthError('Invalid signature. Expected signature base string: %s' % base) |
658 | - built = signature_method.build_signature(oauth_request, consumer, token) |
659 | - |
660 | - def _check_timestamp(self, timestamp): |
661 | - # verify that timestamp is recentish |
662 | - timestamp = int(timestamp) |
663 | - now = int(time.time()) |
664 | - lapsed = now - timestamp |
665 | - if lapsed > self.timestamp_threshold: |
666 | - raise OAuthError('Expired timestamp: given %d and now %s has a greater difference than threshold %d' % (timestamp, now, self.timestamp_threshold)) |
667 | - |
668 | - def _check_nonce(self, consumer, token, nonce): |
669 | - # verify that the nonce is uniqueish |
670 | - nonce = self.data_store.lookup_nonce(consumer, token, nonce) |
671 | - if nonce: |
672 | - raise OAuthError('Nonce already used: %s' % str(nonce)) |
673 | - |
674 | -# OAuthClient is a worker to attempt to execute a request |
675 | -class OAuthClient(object): |
676 | - consumer = None |
677 | - token = None |
678 | - |
679 | - def __init__(self, oauth_consumer, oauth_token): |
680 | - self.consumer = oauth_consumer |
681 | - self.token = oauth_token |
682 | - |
683 | - def get_consumer(self): |
684 | - return self.consumer |
685 | - |
686 | - def get_token(self): |
687 | - return self.token |
688 | - |
689 | - def fetch_request_token(self, oauth_request): |
690 | - # -> OAuthToken |
691 | - raise NotImplementedError |
692 | - |
693 | - def fetch_access_token(self, oauth_request): |
694 | - # -> OAuthToken |
695 | - raise NotImplementedError |
696 | - |
697 | - def access_resource(self, oauth_request): |
698 | - # -> some protected resource |
699 | - raise NotImplementedError |
700 | - |
701 | -# OAuthDataStore is a database abstraction used to lookup consumers and tokens |
702 | -class OAuthDataStore(object): |
703 | - |
704 | - def lookup_consumer(self, key): |
705 | - # -> OAuthConsumer |
706 | - raise NotImplementedError |
707 | - |
708 | - def lookup_token(self, oauth_consumer, token_type, token_token): |
709 | - # -> OAuthToken |
710 | - raise NotImplementedError |
711 | - |
712 | - def lookup_nonce(self, oauth_consumer, oauth_token, nonce, timestamp): |
713 | - # -> OAuthToken |
714 | - raise NotImplementedError |
715 | - |
716 | - def fetch_request_token(self, oauth_consumer): |
717 | - # -> OAuthToken |
718 | - raise NotImplementedError |
719 | - |
720 | - def fetch_access_token(self, oauth_consumer, oauth_token): |
721 | - # -> OAuthToken |
722 | - raise NotImplementedError |
723 | - |
724 | - def authorize_request_token(self, oauth_token, user): |
725 | - # -> OAuthToken |
726 | - raise NotImplementedError |
727 | - |
728 | -# OAuthSignatureMethod is a strategy class that implements a signature method |
729 | -class OAuthSignatureMethod(object): |
730 | - def get_name(self): |
731 | - # -> str |
732 | - raise NotImplementedError |
733 | - |
734 | - def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token): |
735 | - # -> str key, str raw |
736 | - raise NotImplementedError |
737 | - |
738 | - def build_signature(self, oauth_request, oauth_consumer, oauth_token): |
739 | - # -> str |
740 | - raise NotImplementedError |
741 | - |
742 | - def check_signature(self, oauth_request, consumer, token, signature): |
743 | - built = self.build_signature(oauth_request, consumer, token) |
744 | - return built == signature |
745 | - |
746 | -class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod): |
747 | - |
748 | - def get_name(self): |
749 | - return 'HMAC-SHA1' |
750 | - |
751 | - def build_signature_base_string(self, oauth_request, consumer, token): |
752 | - sig = ( |
753 | - escape(oauth_request.get_normalized_http_method()), |
754 | - escape(oauth_request.get_normalized_http_url()), |
755 | - escape(oauth_request.get_normalized_parameters()), |
756 | - ) |
757 | - |
758 | - key = '%s&' % escape(consumer.secret) |
759 | - if token: |
760 | - key += escape(token.secret) |
761 | - raw = '&'.join(sig) |
762 | - return key, raw |
763 | - |
764 | - def build_signature(self, oauth_request, consumer, token): |
765 | - # build the base signature string |
766 | - key, raw = self.build_signature_base_string(oauth_request, consumer, token) |
767 | - |
768 | - # hmac object |
769 | - try: |
770 | - import hashlib # 2.5 |
771 | - hashed = hmac.new(key, raw, hashlib.sha1) |
772 | - except: |
773 | - import sha # deprecated |
774 | - hashed = hmac.new(key, raw, sha) |
775 | - |
776 | - # calculate the digest base 64 |
777 | - return base64.b64encode(hashed.digest()) |
778 | - |
779 | -class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod): |
780 | - |
781 | - def get_name(self): |
782 | - return 'PLAINTEXT' |
783 | - |
784 | - def build_signature_base_string(self, oauth_request, consumer, token): |
785 | - # concatenate the consumer key and secret |
786 | - sig = escape(consumer.secret) |
787 | - if token: |
788 | - sig = '&'.join((sig, escape(token.secret))) |
789 | - return sig |
790 | - |
791 | - def build_signature(self, oauth_request, consumer, token): |
792 | - return self.build_signature_base_string(oauth_request, consumer, token) |
793 | |
794 | === removed directory 'launchpadlib/_utils' |
795 | === removed file 'launchpadlib/_utils/__init__.py' |
796 | === removed file 'launchpadlib/_utils/uri.py' |
797 | --- launchpadlib/_utils/uri.py 2008-08-01 16:05:31 +0000 |
798 | +++ launchpadlib/_utils/uri.py 1970-01-01 00:00:00 +0000 |
799 | @@ -1,578 +0,0 @@ |
800 | -# Copyright 2008 Canonical Ltd. |
801 | - |
802 | -# This file is part of launchpadlib. |
803 | -# |
804 | -# launchpadlib is free software: you can redistribute it and/or modify |
805 | -# it under the terms of the GNU Lesser General Public License as |
806 | -# published by the Free Software Foundation, either version 3 of the |
807 | -# License, or (at your option) any later version. |
808 | -# |
809 | -# launchpadlib is distributed in the hope that it will be useful, but |
810 | -# WITHOUT ANY WARRANTY; without even the implied warranty of |
811 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
812 | -# Lesser General Public License for more details. |
813 | -# |
814 | -# You should have received a copy of the GNU Lesser General Public |
815 | -# License along with launchpadlib. If not, see |
816 | -# <http://www.gnu.org/licenses/>. |
817 | - |
818 | -"""Functions for working with generic syntax URIs.""" |
819 | - |
820 | -__metaclass__ = type |
821 | -__all__ = [ |
822 | - 'URI', |
823 | - 'InvalidURIError', |
824 | - 'find_uris_in_text', |
825 | - 'possible_uri_re'] |
826 | - |
827 | -import re |
828 | - |
829 | - |
830 | -# Default port numbers for different URI schemes |
831 | -# The registered URI schemes comes from |
832 | -# http://www.iana.org/assignments/uri-schemes.html |
833 | -# The default ports come from the relevant RFCs |
834 | - |
835 | -_default_port = { |
836 | - # Official schemes |
837 | - 'acap': '674', |
838 | - 'dav': '80', |
839 | - 'dict': '2628', |
840 | - 'dns': '53', |
841 | - 'ftp': '21', |
842 | - 'go': '1096', |
843 | - 'gopher': '70', |
844 | - 'h323': '1720', |
845 | - 'http': '80', |
846 | - 'https': '443', |
847 | - 'imap': '143', |
848 | - 'ipp': '631', |
849 | - 'iris.beep': '702', |
850 | - 'ldap': '389', |
851 | - 'mtqp': '1038', |
852 | - 'mupdate': '3905', |
853 | - 'nfs': '2049', |
854 | - 'nntp': '119', |
855 | - 'pop': '110', |
856 | - 'rtsp': '554', |
857 | - 'sip': '5060', |
858 | - 'sips': '5061', |
859 | - 'snmp': '161', |
860 | - 'soap.beep': '605', |
861 | - 'soap.beeps': '605', |
862 | - 'telnet': '23', |
863 | - 'tftp': '69', |
864 | - 'tip': '3372', |
865 | - 'vemmi': '575', |
866 | - 'xmlrpc.beep': '602', |
867 | - 'xmlrpc.beeps': '602', |
868 | - 'z39.50r': '210', |
869 | - 'z39.50s': '210', |
870 | - |
871 | - # Historical schemes |
872 | - 'prospero': '1525', |
873 | - 'wais': '210', |
874 | - |
875 | - # Common but unregistered schemes |
876 | - 'bzr+http': '80', |
877 | - 'bzr+ssh': '22', |
878 | - 'irc': '6667', |
879 | - 'sftp': '22', |
880 | - 'ssh': '22', |
881 | - 'svn': '3690', |
882 | - 'svn+ssh': '22', |
883 | - } |
884 | - |
885 | -# Regular expressions adapted from the ABNF in the RFC |
886 | - |
887 | -scheme_re = r"(?P<scheme>[a-z][-a-z0-9+.]*)" |
888 | - |
889 | -userinfo_re = r"(?P<userinfo>(?:[-a-z0-9._~!$&\'()*+,;=:]|%[0-9a-f]{2})*)" |
890 | -# The following regular expression will match some IP address style |
891 | -# host names that the RFC would not (e.g. leading zeros on the |
892 | -# components), but is signficantly simpler. |
893 | -host_re = (r"(?P<host>[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}|" |
894 | - r"(?:[-a-z0-9._~!$&\'()*+,;=]|%[0-9a-f]{2})*|" |
895 | - r"\[[0-9a-z:.]+\])") |
896 | -port_re = r"(?P<port>[0-9]*)" |
897 | - |
898 | -authority_re = r"(?P<authority>(?:%s@)?%s(?::%s)?)" % ( |
899 | - userinfo_re, host_re, port_re) |
900 | - |
901 | -path_abempty_re = r"(?:/(?:[-a-z0-9._~!$&\'()*+,;=:@]|%[0-9a-f]{2})*)*" |
902 | -path_noscheme_re = (r"(?:[-a-z0-9._~!$&\'()*+,;=@]|%[0-9a-f]{2})+" |
903 | - r"(?:/(?:[-a-z0-9._~!$&\'()*+,;=:@]|%[0-9a-f]{2})*)*") |
904 | -path_rootless_re = (r"(?:[-a-z0-9._~!$&\'()*+,;=:@]|%[0-9a-f]{2})+" |
905 | - r"(?:/(?:[-a-z0-9._~!$&\'()*+,;=:@]|%[0-9a-f]{2})*)*") |
906 | -path_absolute_re = r"/(?:%s)?" % path_rootless_re |
907 | -path_empty_re = r"" |
908 | - |
909 | -hier_part_re = r"(?P<hierpart>//%s%s|%s|%s|%s)" % ( |
910 | - authority_re, path_abempty_re, path_absolute_re, path_rootless_re, |
911 | - path_empty_re) |
912 | - |
913 | -relative_part_re = r"(?P<relativepart>//%s%s|%s|%s|%s)" % ( |
914 | - authority_re, path_abempty_re, path_absolute_re, path_noscheme_re, |
915 | - path_empty_re) |
916 | - |
917 | -# Additionally we also permit square braces in the query portion to |
918 | -# accomodate real-world URIs. |
919 | -query_re = r"(?P<query>(?:[-a-z0-9._~!$&\'()*+,;=:@/?\[\]]|%[0-9a-f]{2})*)" |
920 | -fragment_re = r"(?P<fragment>(?:[-a-z0-9._~!$&\'()*+,;=:@/?]|%[0-9a-f]{2})*)" |
921 | - |
922 | -uri_re = r"%s:%s(?:\?%s)?(?:#%s)?$" % ( |
923 | - scheme_re, hier_part_re, query_re, fragment_re) |
924 | - |
925 | -relative_ref_re = r"%s(?:\?%s)?(?:#%s)?$" % ( |
926 | - relative_part_re, query_re, fragment_re) |
927 | - |
928 | -uri_pat = re.compile(uri_re, re.IGNORECASE) |
929 | -relative_ref_pat = re.compile(relative_ref_re, re.IGNORECASE) |
930 | - |
931 | - |
932 | -def merge(basepath, relpath, has_authority): |
933 | - """Merge two URI path components into a single path component. |
934 | - |
935 | - Follows rules specified in Section 5.2.3 of RFC 3986. |
936 | - |
937 | - The algorithm in the RFC treats the empty basepath edge case |
938 | - differently for URIs with and without an authority section, which |
939 | - is why the third argument is necessary. |
940 | - """ |
941 | - if has_authority and basepath == '': |
942 | - return '/' + relpath |
943 | - slash = basepath.rfind('/') |
944 | - return basepath[:slash+1] + relpath |
945 | - |
946 | - |
947 | -def remove_dot_segments(path): |
948 | - """Remove '.' and '..' segments from a URI path. |
949 | - |
950 | - Follows the rules specified in Section 5.2.4 of RFC 3986. |
951 | - """ |
952 | - output = [] |
953 | - while path: |
954 | - if path.startswith('../'): |
955 | - path = path[3:] |
956 | - elif path.startswith('./'): |
957 | - path = path[2:] |
958 | - elif path.startswith('/./') or path == '/.': |
959 | - path = '/' + path[3:] |
960 | - elif path.startswith('/../') or path == '/..': |
961 | - path = '/' + path[4:] |
962 | - if len(output) > 0: |
963 | - del output[-1] |
964 | - elif path in ['.', '..']: |
965 | - path = '' |
966 | - else: |
967 | - if path.startswith('/'): |
968 | - slash = path.find('/', 1) |
969 | - else: |
970 | - slash = path.find('/') |
971 | - if slash < 0: |
972 | - slash = len(path) |
973 | - output.append(path[:slash]) |
974 | - path = path[slash:] |
975 | - return ''.join(output) |
976 | - |
977 | - |
978 | -def normalise_unreserved(string): |
979 | - """Return a version of 's' where no unreserved characters are encoded. |
980 | - |
981 | - Unreserved characters are defined in Section 2.3 of RFC 3986. |
982 | - |
983 | - Percent encoded sequences are normalised to upper case. |
984 | - """ |
985 | - result = string.split('%') |
986 | - unreserved = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' |
987 | - 'abcdefghijklmnopqrstuvwxyz' |
988 | - '0123456789-._~') |
989 | - for index, item in enumerate(result): |
990 | - if index == 0: |
991 | - continue |
992 | - try: |
993 | - ch = int(item[:2], 16) |
994 | - except ValueError: |
995 | - continue |
996 | - if chr(ch) in unreserved: |
997 | - result[index] = chr(ch) + item[2:] |
998 | - else: |
999 | - result[index] = '%%%02X%s' % (ch, item[2:]) |
1000 | - return ''.join(result) |
1001 | - |
1002 | - |
1003 | -class InvalidURIError(Exception): |
1004 | - """Invalid URI""" |
1005 | - |
1006 | - |
1007 | -class URI: |
1008 | - """A class that represents a URI. |
1009 | - |
1010 | - This class can represent arbitrary URIs that conform to the |
1011 | - generic syntax described in RFC 3986. |
1012 | - """ |
1013 | - |
1014 | - def __init__(self, uri=None, scheme=None, userinfo=None, host=None, |
1015 | - port=None, path=None, query=None, fragment=None): |
1016 | - """Create a URI instance. |
1017 | - |
1018 | - Can be called with either a string URI or the component parts |
1019 | - of the URI as keyword arguments. |
1020 | - |
1021 | - In either case, all arguments are expected to be appropriately |
1022 | - URI encoded. |
1023 | - """ |
1024 | - assert (uri is not None and scheme is None and userinfo is None and |
1025 | - host is None and port is None and path is None and |
1026 | - query is None and fragment is None) or uri is None, ( |
1027 | - "URI() must be called with a single string argument or " |
1028 | - "with URI components given as keyword arguments.") |
1029 | - |
1030 | - if uri is not None: |
1031 | - if isinstance(uri, unicode): |
1032 | - try: |
1033 | - uri = uri.encode('ASCII') |
1034 | - except UnicodeEncodeError: |
1035 | - raise InvalidURIError( |
1036 | - 'URIs must consist of ASCII characters') |
1037 | - match = uri_pat.match(uri) |
1038 | - if match is None: |
1039 | - raise InvalidURIError('"%s" is not a valid URI' % uri) |
1040 | - self.scheme = match.group('scheme') |
1041 | - self.userinfo = match.group('userinfo') |
1042 | - self.host = match.group('host') |
1043 | - self.port = match.group('port') |
1044 | - hierpart = match.group('hierpart') |
1045 | - authority = match.group('authority') |
1046 | - if authority is None: |
1047 | - self.path = hierpart |
1048 | - else: |
1049 | - # Skip past the //authority part |
1050 | - self.path = hierpart[2+len(authority):] |
1051 | - self.query = match.group('query') |
1052 | - self.fragment = match.group('fragment') |
1053 | - else: |
1054 | - if scheme is None: |
1055 | - raise InvalidURIError('URIs must have a scheme') |
1056 | - if host is None and (userinfo is not None or port is not None): |
1057 | - raise InvalidURIError( |
1058 | - 'host must be given if userinfo or port are') |
1059 | - if path is None: |
1060 | - raise InvalidURIError('URIs must have a path') |
1061 | - self.scheme = scheme |
1062 | - self.userinfo = userinfo |
1063 | - self.host = host |
1064 | - self.port = port |
1065 | - self.path = path |
1066 | - self.query = query |
1067 | - self.fragment = fragment |
1068 | - |
1069 | - self._normalise() |
1070 | - |
1071 | - if (self.scheme in ['http', 'https', 'ftp', 'gopher', 'telnet', |
1072 | - 'imap', 'mms', 'rtsp', 'svn', 'svn+ssh', |
1073 | - 'bzr', 'bzr+http', 'bzr+ssh'] and |
1074 | - not self.host): |
1075 | - raise InvalidURIError('%s URIs must have a host name' % |
1076 | - self.scheme) |
1077 | - |
1078 | - def _normalise(self): |
1079 | - """Perform normalisation of URI components.""" |
1080 | - self.scheme = self.scheme.lower() |
1081 | - |
1082 | - if self.userinfo is not None: |
1083 | - self.userinfo = normalise_unreserved(self.userinfo) |
1084 | - if self.host is not None: |
1085 | - self.host = normalise_unreserved(self.host.lower()) |
1086 | - if self.port == '': |
1087 | - self.port = None |
1088 | - elif self.port is not None: |
1089 | - if self.port == _default_port.get(self.scheme): |
1090 | - self.port = None |
1091 | - if self.host is not None and self.path == '': |
1092 | - self.path = '/' |
1093 | - self.path = normalise_unreserved(remove_dot_segments(self.path)) |
1094 | - |
1095 | - if self.query is not None: |
1096 | - self.query = normalise_unreserved(self.query) |
1097 | - if self.fragment is not None: |
1098 | - self.fragment = normalise_unreserved(self.fragment) |
1099 | - |
1100 | - @property |
1101 | - def authority(self): |
1102 | - """The authority part of the URI""" |
1103 | - if self.host is None: |
1104 | - return None |
1105 | - authority = self.host |
1106 | - if self.userinfo is not None: |
1107 | - authority = '%s@%s' % (self.userinfo, authority) |
1108 | - if self.port is not None: |
1109 | - authority = '%s:%s' % (authority, self.port) |
1110 | - return authority |
1111 | - |
1112 | - @property |
1113 | - def hier_part(self): |
1114 | - """The hierarchical part of the URI""" |
1115 | - authority = self.authority |
1116 | - if authority is None: |
1117 | - return self.path |
1118 | - else: |
1119 | - return '//%s%s' % (authority, self.path) |
1120 | - |
1121 | - def __str__(self): |
1122 | - uri = '%s:%s' % (self.scheme, self.hier_part) |
1123 | - if self.query is not None: |
1124 | - uri += '?%s' % self.query |
1125 | - if self.fragment is not None: |
1126 | - uri += '#%s' % self.fragment |
1127 | - return uri |
1128 | - |
1129 | - def __repr__(self): |
1130 | - return '%s(%r)' % (self.__class__.__name__, str(self)) |
1131 | - |
1132 | - def __eq__(self, other): |
1133 | - if isinstance(other, self.__class__): |
1134 | - return (self.scheme == other.scheme and |
1135 | - self.authority == other.authority and |
1136 | - self.path == other.path and |
1137 | - self.query == other.query and |
1138 | - self.fragment == other.fragment) |
1139 | - else: |
1140 | - return NotImplemented |
1141 | - |
1142 | - def __ne__(self, other): |
1143 | - equal = self.__eq__(other) |
1144 | - if equal == NotImplemented: |
1145 | - return NotImplemented |
1146 | - else: |
1147 | - return not equal |
1148 | - |
1149 | - def replace(self, **parts): |
1150 | - """Replace one or more parts of the URI, returning the result.""" |
1151 | - if not parts: |
1152 | - return self |
1153 | - baseparts = dict( |
1154 | - scheme=self.scheme, |
1155 | - userinfo=self.userinfo, |
1156 | - host=self.host, |
1157 | - port=self.port, |
1158 | - path=self.path, |
1159 | - query=self.query, |
1160 | - fragment=self.fragment) |
1161 | - baseparts.update(parts) |
1162 | - return self.__class__(**baseparts) |
1163 | - |
1164 | - def resolve(self, reference): |
1165 | - """Resolve the given URI reference relative to this URI. |
1166 | - |
1167 | - Uses the rules from Section 5.2 of RFC 3986 to resolve the new |
1168 | - URI. |
1169 | - """ |
1170 | - # If the reference is a full URI, then return it as is. |
1171 | - try: |
1172 | - return self.__class__(reference) |
1173 | - except InvalidURIError: |
1174 | - pass |
1175 | - |
1176 | - match = relative_ref_pat.match(reference) |
1177 | - if match is None: |
1178 | - raise InvalidURIError("Invalid relative reference") |
1179 | - |
1180 | - parts = dict(scheme=self.scheme) |
1181 | - authority = match.group('authority') |
1182 | - if authority is not None: |
1183 | - parts['userinfo'] = match.group('userinfo') |
1184 | - parts['host'] = match.group('host') |
1185 | - parts['port'] = match.group('port') |
1186 | - # Skip over the //authority part |
1187 | - parts['path'] = remove_dot_segments( |
1188 | - match.group('relativepart')[2+len(authority):]) |
1189 | - parts['query'] = match.group('query') |
1190 | - else: |
1191 | - path = match.group('relativepart') |
1192 | - query = match.group('query') |
1193 | - if path == '': |
1194 | - parts['path'] = self.path |
1195 | - if query is not None: |
1196 | - parts['query'] = query |
1197 | - else: |
1198 | - parts['query'] = self.query |
1199 | - else: |
1200 | - if path.startswith('/'): |
1201 | - parts['path'] = remove_dot_segments(path) |
1202 | - else: |
1203 | - parts['path'] = merge(self.path, path, |
1204 | - has_authority=self.host is not None) |
1205 | - parts['path'] = remove_dot_segments(parts['path']) |
1206 | - parts['query'] = query |
1207 | - parts['userinfo'] = self.userinfo |
1208 | - parts['host'] = self.host |
1209 | - parts['port'] = self.port |
1210 | - parts['fragment'] = match.group('fragment') |
1211 | - |
1212 | - return self.__class__(**parts) |
1213 | - |
1214 | - def append(self, path): |
1215 | - """Append the given path to this URI. |
1216 | - |
1217 | - The path must not start with a slash, but a slash is added to |
1218 | - base URI (before appending the path), in case it doesn't end |
1219 | - with a slash. |
1220 | - """ |
1221 | - assert not path.startswith('/') |
1222 | - return self.ensureSlash().resolve(path) |
1223 | - |
1224 | - def contains(self, other): |
1225 | - """Returns True if the URI 'other' is contained by this one.""" |
1226 | - if (self.scheme != other.scheme or |
1227 | - self.authority != other.authority): |
1228 | - return False |
1229 | - if self.path == other.path: |
1230 | - return True |
1231 | - basepath = self.path |
1232 | - if not basepath.endswith('/'): |
1233 | - basepath += '/' |
1234 | - otherpath = other.path |
1235 | - if not otherpath.endswith('/'): |
1236 | - otherpath += '/' |
1237 | - return otherpath.startswith(basepath) |
1238 | - |
1239 | - def underDomain(self, domain): |
1240 | - """Return True if the given domain name a parent of the URL's host.""" |
1241 | - if len(domain) == 0: |
1242 | - return True |
1243 | - our_segments = self.host.split('.') |
1244 | - domain_segments = domain.split('.') |
1245 | - return our_segments[-len(domain_segments):] == domain_segments |
1246 | - |
1247 | - def ensureSlash(self): |
1248 | - """Return a URI with the path normalised to end with a slash.""" |
1249 | - if self.path.endswith('/'): |
1250 | - return self |
1251 | - else: |
1252 | - return self.replace(path=self.path + '/') |
1253 | - |
1254 | - def ensureNoSlash(self): |
1255 | - """Return a URI with the path normalised to not end with a slash.""" |
1256 | - if self.path.endswith('/'): |
1257 | - return self.replace(path=self.path.rstrip('/')) |
1258 | - else: |
1259 | - return self |
1260 | - |
1261 | - |
1262 | -# Regular expression for finding URIs in a body of text: |
1263 | -# |
1264 | -# From RFC 3986 ABNF for URIs: |
1265 | -# |
1266 | -# URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ] |
1267 | -# hier-part = "//" authority path-abempty |
1268 | -# / path-absolute |
1269 | -# / path-rootless |
1270 | -# / path-empty |
1271 | -# |
1272 | -# authority = [ userinfo "@" ] host [ ":" port ] |
1273 | -# userinfo = *( unreserved / pct-encoded / sub-delims / ":" ) |
1274 | -# host = IP-literal / IPv4address / reg-name |
1275 | -# reg-name = *( unreserved / pct-encoded / sub-delims ) |
1276 | -# port = *DIGIT |
1277 | -# |
1278 | -# path-abempty = *( "/" segment ) |
1279 | -# path-absolute = "/" [ segment-nz *( "/" segment ) ] |
1280 | -# path-rootless = segment-nz *( "/" segment ) |
1281 | -# path-empty = 0<pchar> |
1282 | -# |
1283 | -# segment = *pchar |
1284 | -# segment-nz = 1*pchar |
1285 | -# pchar = unreserved / pct-encoded / sub-delims / ":" / "@" |
1286 | -# |
1287 | -# query = *( pchar / "/" / "?" ) |
1288 | -# fragment = *( pchar / "/" / "?" ) |
1289 | -# |
1290 | -# unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" |
1291 | -# pct-encoded = "%" HEXDIG HEXDIG |
1292 | -# sub-delims = "!" / "$" / "&" / "'" / "(" / ")" |
1293 | -# / "*" / "+" / "," / ";" / "=" |
1294 | -# |
1295 | -# We only match a set of known scheme names. We don't handle |
1296 | -# IP-literal either. |
1297 | -# |
1298 | -# We will simplify "unreserved / pct-encoded / sub-delims" as the |
1299 | -# following regular expression: |
1300 | -# [-a-zA-Z0-9._~%!$&'()*+,;=] |
1301 | -# |
1302 | -# We also require that the path-rootless form not begin with a |
1303 | -# colon to avoid matching strings like "http::foo" (to avoid bug |
1304 | -# #40255). |
1305 | -# |
1306 | -# The path-empty pattern is not matched either, due to false |
1307 | -# positives. |
1308 | -# |
1309 | -# Some allowed URI punctuation characters will be trimmed if they |
1310 | -# appear at the end of the URI since they may be incidental in the |
1311 | -# flow of the text. |
1312 | -# |
1313 | -# apport has at one time produced query strings containing sqaure |
1314 | -# braces (that are not percent-encoded). In RFC 2986 they seem to be |
1315 | -# allowed by section 2.2 "Reserved Characters", yet section 3.4 |
1316 | -# "Query" appears to provide a strict definition of the query string |
1317 | -# that would forbid square braces. Either way, links with |
1318 | -# non-percent-encoded square braces are being used on Launchpad so |
1319 | -# it's probably best to accomodate them. |
1320 | - |
1321 | -possible_uri_re = r''' |
1322 | -\b |
1323 | -(?:about|gopher|http|https|sftp|news|ftp|mailto|file|irc|jabber|xmpp) |
1324 | -: |
1325 | -(?: |
1326 | - (?: |
1327 | - # "//" authority path-abempty |
1328 | - // |
1329 | - (?: # userinfo |
1330 | - [%(unreserved)s:]* |
1331 | - @ |
1332 | - )? |
1333 | - (?: # host |
1334 | - \d+\.\d+\.\d+\.\d+ | |
1335 | - [%(unreserved)s]* |
1336 | - ) |
1337 | - (?: # port |
1338 | - : \d* |
1339 | - )? |
1340 | - (?: / [%(unreserved)s:@]* )* |
1341 | - ) | (?: |
1342 | - # path-absolute |
1343 | - / |
1344 | - (?: [%(unreserved)s:@]+ |
1345 | - (?: / [%(unreserved)s:@]* )* )? |
1346 | - ) | (?: |
1347 | - # path-rootless |
1348 | - [%(unreserved)s@] |
1349 | - [%(unreserved)s:@]* |
1350 | - (?: / [%(unreserved)s:@]* )* |
1351 | - ) |
1352 | -) |
1353 | -(?: # query |
1354 | - \? |
1355 | - [%(unreserved)s:@/\?\[\]]* |
1356 | -)? |
1357 | -(?: # fragment |
1358 | - \# |
1359 | - [%(unreserved)s:@/\?]* |
1360 | -)? |
1361 | -''' % {'unreserved': "-a-zA-Z0-9._~%!$&'()*+,;="} |
1362 | - |
1363 | -possible_uri_pat = re.compile(possible_uri_re, re.IGNORECASE | re.VERBOSE) |
1364 | -uri_trailers_pat = re.compile(r'([,.?:);>]+)$') |
1365 | - |
1366 | -def find_uris_in_text(text): |
1367 | - """Scan a block of text for URIs, and yield the ones found.""" |
1368 | - for match in possible_uri_pat.finditer(text): |
1369 | - uri_string = match.group() |
1370 | - # remove characters from end of URI that are not likely to be |
1371 | - # part of the URI. |
1372 | - uri_string = uri_trailers_pat.sub('', uri_string) |
1373 | - try: |
1374 | - uri = URI(uri_string) |
1375 | - except InvalidURIError: |
1376 | - continue |
1377 | - yield uri |
1378 | |
1379 | === modified file 'setup.py' |
1380 | --- setup.py 2008-10-07 15:59:44 +0000 |
1381 | +++ setup.py 2009-03-19 18:27:46 +0000 |
1382 | @@ -1,6 +1,7 @@ |
1383 | #!/usr/bin/env python |
1384 | -# Copyright 2008 Canonical Ltd. |
1385 | |
1386 | +# Copyright 2008-2009 Canonical Ltd. |
1387 | +# |
1388 | # This file is part of launchpadlib. |
1389 | # |
1390 | # launchpadlib is free software: you can redistribute it and/or modify |
1391 | @@ -22,31 +23,58 @@ |
1392 | import ez_setup |
1393 | ez_setup.use_setuptools() |
1394 | |
1395 | +import sys |
1396 | from setuptools import setup, find_packages |
1397 | + |
1398 | +# generic helpers primarily for the long_description |
1399 | +def generate(*docname_or_string): |
1400 | + res = [] |
1401 | + for value in docname_or_string: |
1402 | + if value.endswith('.txt'): |
1403 | + f = open(value) |
1404 | + value = f.read() |
1405 | + f.close() |
1406 | + res.append(value) |
1407 | + if not value.endswith('\n'): |
1408 | + res.append('') |
1409 | + return '\n'.join(res) |
1410 | +# end generic helpers |
1411 | + |
1412 | +sys.path.insert(0, 'src') |
1413 | from launchpadlib import __version__ |
1414 | |
1415 | - |
1416 | setup( |
1417 | - name = 'launchpadlib', |
1418 | - version = __version__, |
1419 | - description = """\ |
1420 | -launchpadlib is a client-side library for scripting Launchpad through its web |
1421 | -services interface. Launchpad <http://launchpad.net> is a a free software |
1422 | -hosting and development website, making it easy to collaborate across multiple |
1423 | -projects.""", |
1424 | - author = 'The Launchpad developers', |
1425 | - author_email= 'launchpadlib@lists.launchpad.net', |
1426 | - url = 'https://help.launchpad.net/API/launchpadlib', |
1427 | - license = 'GNU LGPLv3 or later', |
1428 | + name='launchpadlib', |
1429 | + version=__version__, |
1430 | + packages=find_packages('src'), |
1431 | + package_dir={'':'src'}, |
1432 | + include_package_data=True, |
1433 | + zip_safe=False, |
1434 | + author='The Launchpad developers', |
1435 | + author_email='launchpadlib@lists.launchpad.net', |
1436 | + maintainer='LAZR Developers', |
1437 | + maintainer_email='lazr-developers@lists.launchpad.net', |
1438 | download_url= 'https://launchpad.net/launchpadlib/+download', |
1439 | - packages = find_packages(), |
1440 | - include_package_data = True, |
1441 | - zip_safe = True, |
1442 | - install_requires = [ |
1443 | + description=open('README.txt').readline().strip(), |
1444 | + long_description=generate( |
1445 | + 'src/launchpadlib/README.txt', |
1446 | + 'src/launchpadlib/NEWS.txt'), |
1447 | + license='LGPL v3', |
1448 | + install_requires=[ |
1449 | 'httplib2', |
1450 | + 'lazr.uri', |
1451 | + 'oauth', |
1452 | + 'setuptools', |
1453 | 'simplejson', |
1454 | + 'wadllib', |
1455 | ], |
1456 | - setup_requires = [ |
1457 | - 'setuptools_bzr', |
1458 | - ] |
1459 | + url='https://help.launchpad.net/API/launchpadlib', |
1460 | + classifiers=[ |
1461 | + "Development Status :: 5 - Production/Stable", |
1462 | + "Intended Audience :: Developers", |
1463 | + "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", |
1464 | + "Operating System :: OS Independent", |
1465 | + "Programming Language :: Python"], |
1466 | + setup_requires=['eggtestinfo', 'setuptools_bzr'], |
1467 | + # test_suite='launchpadlib.tests.test_suite', |
1468 | ) |
1469 | |
1470 | === added directory 'src' |
1471 | === renamed directory 'launchpadlib' => 'src/launchpadlib' |
1472 | === added file 'src/launchpadlib/NEWS.txt' |
1473 | --- src/launchpadlib/NEWS.txt 1970-01-01 00:00:00 +0000 |
1474 | +++ src/launchpadlib/NEWS.txt 2009-03-16 21:02:52 +0000 |
1475 | @@ -0,0 +1,8 @@ |
1476 | +===================== |
1477 | +NEWS for launchpadlib |
1478 | +===================== |
1479 | + |
1480 | +1.0 (200X-XX-XX) |
1481 | +================ |
1482 | + |
1483 | +- Initial release |
1484 | |
1485 | === added file 'src/launchpadlib/README.txt' |
1486 | --- src/launchpadlib/README.txt 1970-01-01 00:00:00 +0000 |
1487 | +++ src/launchpadlib/README.txt 2009-03-16 21:02:52 +0000 |
1488 | @@ -0,0 +1,20 @@ |
1489 | +.. |
1490 | + This file is part of launchpadlib. |
1491 | + |
1492 | + launchpadlib is free software: you can redistribute it and/or modify it |
1493 | + under the terms of the GNU Lesser General Public License as published by |
1494 | + the Free Software Foundation, either version 3 of the License, or (at your |
1495 | + option) any later version. |
1496 | + |
1497 | + launchpadlib is distributed in the hope that it will be useful, but |
1498 | + WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY |
1499 | + or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public |
1500 | + License for more details. |
1501 | + |
1502 | + You should have received a copy of the GNU Lesser General Public License |
1503 | + along with launchpadlib. If not, see <http://www.gnu.org/licenses/>. |
1504 | + |
1505 | +launchpadlib |
1506 | +************ |
1507 | + |
1508 | +See https://help.launchpad.net/API/launchpadlib . |
1509 | |
1510 | === modified file 'src/launchpadlib/_browser.py' |
1511 | --- launchpadlib/_browser.py 2009-02-17 15:42:25 +0000 |
1512 | +++ src/launchpadlib/_browser.py 2009-03-19 15:22:56 +0000 |
1513 | @@ -29,22 +29,22 @@ |
1514 | |
1515 | |
1516 | import atexit |
1517 | +from cStringIO import StringIO |
1518 | import gzip |
1519 | -import shutil |
1520 | -import tempfile |
1521 | from httplib2 import ( |
1522 | FailedToDecompressContent, FileCache, Http, safename, urlnorm) |
1523 | +from lazr.uri import URI |
1524 | +from oauth.oauth import ( |
1525 | + OAuthRequest, OAuthSignatureMethod_PLAINTEXT) |
1526 | +import shutil |
1527 | import simplejson |
1528 | -from cStringIO import StringIO |
1529 | -import zlib |
1530 | - |
1531 | +import tempfile |
1532 | from urllib import urlencode |
1533 | from wadllib.application import Application |
1534 | +import zlib |
1535 | + |
1536 | from launchpadlib.errors import HTTPError |
1537 | -from launchpadlib._oauth.oauth import ( |
1538 | - OAuthRequest, OAuthSignatureMethod_PLAINTEXT) |
1539 | -from launchpadlib._utils import uri, json |
1540 | -from launchpadlib._utils.json import DatetimeJSONEncoder |
1541 | +from launchpadlib._json import DatetimeJSONEncoder |
1542 | |
1543 | |
1544 | OAUTH_REALM = 'https://api.launchpad.net' |
1545 | @@ -214,7 +214,7 @@ |
1546 | |
1547 | def get(self, resource_or_uri, headers=None, return_response=False): |
1548 | """GET a representation of the given resource or URI.""" |
1549 | - if isinstance(resource_or_uri, (basestring, uri.URI)): |
1550 | + if isinstance(resource_or_uri, (basestring, URI)): |
1551 | url = resource_or_uri |
1552 | else: |
1553 | method = resource_or_uri.get_method('get') |
1554 | @@ -262,5 +262,6 @@ |
1555 | headers['If-Match'] = cached_etag |
1556 | |
1557 | return self._request( |
1558 | - url, simplejson.dumps(representation, cls=DatetimeJSONEncoder), |
1559 | + url, simplejson.dumps(representation, |
1560 | + cls=DatetimeJSONEncoder), |
1561 | 'PATCH', extra_headers=extra_headers) |
1562 | |
1563 | === renamed file 'launchpadlib/_utils/json.py' => 'src/launchpadlib/_json.py' |
1564 | === modified file 'src/launchpadlib/credentials.py' |
1565 | --- launchpadlib/credentials.py 2008-08-01 17:30:37 +0000 |
1566 | +++ src/launchpadlib/credentials.py 2009-03-19 15:22:56 +0000 |
1567 | @@ -25,13 +25,13 @@ |
1568 | 'Credentials', |
1569 | ] |
1570 | |
1571 | +from ConfigParser import SafeConfigParser |
1572 | import cgi |
1573 | import httplib2 |
1574 | +from oauth.oauth import OAuthConsumer, OAuthToken |
1575 | from urllib import urlencode |
1576 | |
1577 | -from ConfigParser import SafeConfigParser |
1578 | from launchpadlib.errors import CredentialsFileError, HTTPError |
1579 | -from launchpadlib._oauth.oauth import OAuthConsumer, OAuthToken |
1580 | |
1581 | |
1582 | CREDENTIALS_FILE_VERSION = '1' |
1583 | |
1584 | === modified file 'src/launchpadlib/launchpad.py' |
1585 | --- launchpadlib/launchpad.py 2008-08-22 14:03:54 +0000 |
1586 | +++ src/launchpadlib/launchpad.py 2009-03-16 21:02:52 +0000 |
1587 | @@ -28,9 +28,9 @@ |
1588 | import webbrowser |
1589 | |
1590 | from wadllib.application import Resource as WadlResource |
1591 | +from lazr.uri import URI |
1592 | |
1593 | from launchpadlib._browser import Browser |
1594 | -from launchpadlib._utils.uri import URI |
1595 | from launchpadlib.resource import Resource |
1596 | from launchpadlib.credentials import AccessToken, Credentials |
1597 | |
1598 | |
1599 | === modified file 'src/launchpadlib/resource.py' |
1600 | --- launchpadlib/resource.py 2009-02-17 15:42:25 +0000 |
1601 | +++ src/launchpadlib/resource.py 2009-03-19 15:22:56 +0000 |
1602 | @@ -32,9 +32,9 @@ |
1603 | from StringIO import StringIO |
1604 | import urllib |
1605 | from urlparse import urlparse |
1606 | +from lazr.uri import URI |
1607 | |
1608 | -from launchpadlib._utils.uri import URI |
1609 | -from launchpadlib._utils.json import DatetimeJSONEncoder |
1610 | +from launchpadlib._json import DatetimeJSONEncoder |
1611 | from launchpadlib.errors import HTTPError |
1612 | from wadllib.application import Resource as WadlResource |
1613 |
This makes launchpadlib built with buildout.
There are no tests or documents in the package, so this package looks more bare than usual.
This requires lazr.uri and the wadllib package that I also have up for review. See https:/ /dev.launchpad. net/Hacking. Build lazr.uri and wadllib by checking them out, ``cd``'ing into the directory, running ``python bootstrap.py``, running ``./bin/buildout``, and running ``./bin/buildout setup . sdist``. Then put the .tgz file in the download-cache/dist directory.
Thanks
Gary