Merge lp://staging/~gary/launchpadlib/buildout into lp://staging/~launchpad-pqm/launchpadlib/devel

Proposed by Gary Poster
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
Reviewer Review Type Date Requested Status
Francis J. Lacoste (community) Approve
LAZR Developers Pending
launchpadlib developers Pending
Review via email: mp+4675@code.staging.launchpad.net
To post a comment you must log in.
Revision history for this message
Gary Poster (gary) wrote :

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

Revision history for this message
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-developers)
>
> 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.
>

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._utils.uri into a separate cheeseshop package
> - 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/launchpadlib/NEWS.txt 1970-01-01 00:00:00 +0000
> +++ src/launchpadlib/NEWS.txt 2009-03-16 21:02:52 +0000
> @@ -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>

review: Approve
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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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

Subscribers

People subscribed via source and target branches