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
=== modified file '.bzrignore'
--- .bzrignore 2009-02-04 07:30:54 +0000
+++ .bzrignore 2009-03-19 15:22:56 +0000
@@ -1,4 +1,11 @@
1bin
2develop-eggs
3.installed.cfg
4develop-eggs
5parts
6*.egg-info
7tags
8TAGS
1build9build
10*.egg
2dist11dist
3launchpadlib.egg-info
4setuptools_bzr-1.2-py2.5.egg
512
=== added file 'HACKING.txt'
--- HACKING.txt 1970-01-01 00:00:00 +0000
+++ HACKING.txt 2009-03-16 21:02:52 +0000
@@ -0,0 +1,42 @@
1..
2 This file is part of lazr.launchpadlib.
3
4 lazr.launchpadlib is free software: you can redistribute it and/or modify it
5 under the terms of the GNU Lesser General Public License as published by
6 the Free Software Foundation, either version 3 of the License, or (at your
7 option) any later version.
8
9 lazr.launchpadlib is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
12 License for more details.
13
14 You should have received a copy of the GNU Lesser General Public License
15 along with lazr.launchpadlib. If not, see <http://www.gnu.org/licenses/>.
16
17This project uses zc.buildout for development.
18
19============
20Introduction
21============
22
23These are guidelines for hacking on the lazr.launchpadlib project. But first,
24please see the common hacking guidelines at:
25
26 http://dev.launchpad.net/Hacking
27
28
29Getting help
30------------
31
32If you find bugs in this package, you can report them here:
33
34 https://launchpad.net/lazr.launchpadlib
35
36If you want to discuss this package, join the team and mailing list here:
37
38 https://launchpad.net/~lazr-developers
39
40or send a message to:
41
42 lazr-developers@lists.launchpad.net
043
=== modified file 'README.txt'
--- README.txt 2008-08-01 16:05:31 +0000
+++ README.txt 2009-03-16 21:02:52 +0000
@@ -1,37 +1,38 @@
1Copyright (C) 2008 Canonical Ltd.1Script Launchpad through its web services interfaces. Officially supported.
22
3This file is part of launchpadlib.3..
44 Copyright (C) 2008-2009 Canonical Ltd.
5launchpadlib is free software: you can redistribute it and/or modify it5
6under the terms of the GNU Lesser General Public License as published6 This file is part of launchpadlib.
7by the Free Software Foundation, either version 3 of the License, or7
8(at your option) any later version.8 launchpadlib is free software: you can redistribute it and/or modify it
99 under the terms of the GNU Lesser General Public License as published by
10launchpadlib is distributed in the hope that it will be useful, but WITHOUT10 the Free Software Foundation, either version 3 of the License, or (at your
11ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or11 option) any later version.
12FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public12
13License for more details.13 launchpadlib is distributed in the hope that it will be useful, but WITHOUT
1414 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
15You should have received a copy of the GNU Lesser General Public15 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
16License along with launchpadlib. If not, see16 License for more details.
17<http://www.gnu.org/licenses/>.17
1818 You should have received a copy of the GNU Lesser General Public License
1919 along with launchpadlib. If not, see <http://www.gnu.org/licenses/>.
20== Overview ==20
21
22Overview
23********
2124
22launchpadlib is a standalone Python library for scripting Launchpad through25launchpadlib is a standalone Python library for scripting Launchpad through
23its web services interface. It is the officially supported bindings to the26its web services interface. It is the officially supported bindings to the
24Launchpad web service, but there may be third party bindings that provide27Launchpad web service, but there may be third party bindings that provide
25scriptability for other languages.28scriptability for other languages.
2629
27For information on Launchpad itself, see30Launchpad (http://launchpad.net) is a a free software hosting and development
31website, making it easy to collaborate across multiple projects. For
32information on Launchpad itself, see
2833
29 https://help.launchpad.net34 https://help.launchpad.net
3035
31Launchpad is available at
32
33 https://launchpad.net
34
35More information on the Launchpad web service, such as user guides and36More information on the Launchpad web service, such as user guides and
36reference documentation, are available at37reference documentation, are available at
3738
3839
=== 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 @@
4- make == and is work4- make == and is work
5- integrate httplib2 caching; conditional get; conditional patch5- integrate httplib2 caching; conditional get; conditional patch
6- Flesh out HTTPErrors as per Francis email6- Flesh out HTTPErrors as per Francis email
7- Move launchpadlib._utils.uri into a separate cheeseshop package
8- Support hosted file resources7- Support hosted file resources
98
=== added file 'bootstrap.py'
--- bootstrap.py 1970-01-01 00:00:00 +0000
+++ bootstrap.py 2009-03-16 21:02:52 +0000
@@ -0,0 +1,77 @@
1##############################################################################
2#
3# Copyright (c) 2006 Zope Corporation and Contributors.
4# All Rights Reserved.
5#
6# This software is subject to the provisions of the Zope Public License,
7# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
8# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
9# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
10# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
11# FOR A PARTICULAR PURPOSE.
12#
13##############################################################################
14"""Bootstrap a buildout-based project
15
16Simply run this script in a directory containing a buildout.cfg.
17The script accepts buildout command-line options, so you can
18use the -c option to specify an alternate configuration file.
19
20$Id$
21"""
22
23import os, shutil, sys, tempfile, urllib2
24
25tmpeggs = tempfile.mkdtemp()
26
27is_jython = sys.platform.startswith('java')
28
29try:
30 import pkg_resources
31except ImportError:
32 ez = {}
33 exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py'
34 ).read() in ez
35 ez['use_setuptools'](to_dir=tmpeggs, download_delay=0)
36
37 import pkg_resources
38
39if sys.platform == 'win32':
40 def quote(c):
41 if ' ' in c:
42 return '"%s"' % c # work around spawn lamosity on windows
43 else:
44 return c
45else:
46 def quote (c):
47 return c
48
49cmd = 'from setuptools.command.easy_install import main; main()'
50ws = pkg_resources.working_set
51
52if is_jython:
53 import subprocess
54
55 assert subprocess.Popen([sys.executable] + ['-c', quote(cmd), '-mqNxd',
56 quote(tmpeggs), 'zc.buildout'],
57 env=dict(os.environ,
58 PYTHONPATH=
59 ws.find(pkg_resources.Requirement.parse('setuptools')).location
60 ),
61 ).wait() == 0
62
63else:
64 assert os.spawnle(
65 os.P_WAIT, sys.executable, quote (sys.executable),
66 '-c', quote (cmd), '-mqNxd', quote (tmpeggs), 'zc.buildout',
67 dict(os.environ,
68 PYTHONPATH=
69 ws.find(pkg_resources.Requirement.parse('setuptools')).location
70 ),
71 ) == 0
72
73ws.add_entry(tmpeggs)
74ws.require('zc.buildout')
75import zc.buildout.buildout
76zc.buildout.buildout.main(sys.argv[1:] + ['bootstrap'])
77shutil.rmtree(tmpeggs)
078
=== added file 'buildout.cfg'
--- buildout.cfg 1970-01-01 00:00:00 +0000
+++ buildout.cfg 2009-03-19 15:22:56 +0000
@@ -0,0 +1,31 @@
1[buildout]
2parts =
3 interpreter
4# test
5# docs
6 tags
7unzip = true
8
9develop = .
10
11[test]
12recipe = zc.recipe.testrunner
13eggs = launchpadlib
14defaults = '--tests-pattern ^tests --exit-with-status --suite-name additional_tests'.split()
15
16[docs]
17recipe = z3c.recipe.sphinxdoc
18eggs = launchpadlib [docs]
19index-doc = README
20default.css =
21layout.html =
22
23[interpreter]
24recipe = zc.recipe.egg
25interpreter = py
26eggs = launchpadlib
27 docutils
28
29[tags]
30recipe = z3c.recipe.tag:tags
31eggs = launchpadlib
032
=== removed directory 'launchpadlib/_oauth'
=== removed file 'launchpadlib/_oauth/__init__.py'
=== removed file 'launchpadlib/_oauth/oauth.py'
--- launchpadlib/_oauth/oauth.py 2008-08-01 15:45:31 +0000
+++ launchpadlib/_oauth/oauth.py 1970-01-01 00:00:00 +0000
@@ -1,525 +0,0 @@
1import cgi
2import urllib
3import time
4import random
5import urlparse
6import hmac
7import base64
8
9VERSION = '1.0' # Hi Blaine!
10HTTP_METHOD = 'GET'
11SIGNATURE_METHOD = 'PLAINTEXT'
12
13# Generic exception class
14class OAuthError(RuntimeError):
15 def __init__(self, message='OAuth error occured.'):
16 self.message = message
17
18# optional WWW-Authenticate header (401 error)
19def build_authenticate_header(realm=''):
20 return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
21
22# url escape
23def escape(s):
24 # escape '/' too
25 return urllib.quote(s, safe='~')
26
27# util function: current timestamp
28# seconds since epoch (UTC)
29def generate_timestamp():
30 return int(time.time())
31
32# util function: nonce
33# pseudorandom number
34def generate_nonce(length=8):
35 return ''.join(str(random.randint(0, 9)) for i in range(length))
36
37# OAuthConsumer is a data type that represents the identity of the Consumer
38# via its shared secret with the Service Provider.
39class OAuthConsumer(object):
40 key = None
41 secret = None
42
43 def __init__(self, key, secret):
44 self.key = key
45 self.secret = secret
46
47# OAuthToken is a data type that represents an End User via either an access
48# or request token.
49class OAuthToken(object):
50 # access tokens and request tokens
51 key = None
52 secret = None
53
54 '''
55 key = the token
56 secret = the token secret
57 '''
58 def __init__(self, key, secret):
59 self.key = key
60 self.secret = secret
61
62 def to_string(self):
63 return urllib.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret})
64
65 # return a token from something like:
66 # oauth_token_secret=digg&oauth_token=digg
67 @staticmethod
68 def from_string(s):
69 params = cgi.parse_qs(s, keep_blank_values=False)
70 key = params['oauth_token'][0]
71 secret = params['oauth_token_secret'][0]
72 return OAuthToken(key, secret)
73
74 def __str__(self):
75 return self.to_string()
76
77# OAuthRequest represents the request and can be serialized
78class OAuthRequest(object):
79 '''
80 OAuth parameters:
81 - oauth_consumer_key
82 - oauth_token
83 - oauth_signature_method
84 - oauth_signature
85 - oauth_timestamp
86 - oauth_nonce
87 - oauth_version
88 ... any additional parameters, as defined by the Service Provider.
89 '''
90 parameters = None # oauth parameters
91 http_method = HTTP_METHOD
92 http_url = None
93 version = VERSION
94
95 def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None):
96 self.http_method = http_method
97 self.http_url = http_url
98 self.parameters = parameters or {}
99
100 def set_parameter(self, parameter, value):
101 self.parameters[parameter] = value
102
103 def get_parameter(self, parameter):
104 try:
105 return self.parameters[parameter]
106 except:
107 raise OAuthError('Parameter not found: %s' % parameter)
108
109 def _get_timestamp_nonce(self):
110 return self.get_parameter('oauth_timestamp'), self.get_parameter('oauth_nonce')
111
112 # get any non-oauth parameters
113 def get_nonoauth_parameters(self):
114 parameters = {}
115 for k, v in self.parameters.iteritems():
116 # ignore oauth parameters
117 if k.find('oauth_') < 0:
118 parameters[k] = v
119 return parameters
120
121 # serialize as a header for an HTTPAuth request
122 def to_header(self, realm=''):
123 auth_header = 'OAuth realm="%s"' % realm
124 # add the oauth parameters
125 if self.parameters:
126 for k, v in self.parameters.iteritems():
127 auth_header += ', %s="%s"' % (k, v)
128 return {'Authorization': auth_header}
129
130 # serialize as post data for a POST request
131 def to_postdata(self):
132 return '&'.join('%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.iteritems())
133
134 # serialize as a url for a GET request
135 def to_url(self):
136 return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata())
137
138 # return a string that consists of all the parameters that need to be signed
139 def get_normalized_parameters(self):
140 params = self.parameters
141 try:
142 # exclude the signature if it exists
143 del params['oauth_signature']
144 except:
145 pass
146 key_values = params.items()
147 # sort lexicographically, first after key, then after value
148 key_values.sort()
149 # combine key value pairs in string and escape
150 return '&'.join('%s=%s' % (escape(str(k)), escape(str(v))) for k, v in key_values)
151
152 # just uppercases the http method
153 def get_normalized_http_method(self):
154 return self.http_method.upper()
155
156 # parses the url and rebuilds it to be scheme://host/path
157 def get_normalized_http_url(self):
158 parts = urlparse.urlparse(self.http_url)
159 url_string = '%s://%s%s' % (parts[0], parts[1], parts[2]) # scheme, netloc, path
160 return url_string
161
162 # set the signature parameter to the result of build_signature
163 def sign_request(self, signature_method, consumer, token):
164 # set the signature method
165 self.set_parameter('oauth_signature_method', signature_method.get_name())
166 # set the signature
167 self.set_parameter('oauth_signature', self.build_signature(signature_method, consumer, token))
168
169 def build_signature(self, signature_method, consumer, token):
170 # call the build signature method within the signature method
171 return signature_method.build_signature(self, consumer, token)
172
173 @staticmethod
174 def from_request(http_method, http_url, headers=None, postdata=None, parameters=None):
175
176 # let the library user override things however they'd like, if they know
177 # which parameters to use then go for it, for example XMLRPC might want to
178 # do this
179 if parameters is not None:
180 return OAuthRequest(http_method, http_url, parameters)
181
182 # from the headers
183 if headers and 'Authorization' in headers:
184 try:
185 auth_header = headers['Authorization']
186 # check that the authorization header is OAuth
187 auth_header.index('OAuth')
188 # get the parameters from the header
189 parameters = OAuthRequest._split_header(auth_header)
190 return OAuthRequest(http_method, http_url, parameters)
191 except:
192 raise OAuthError('Unable to parse OAuth parameters from Authorization header.')
193
194 # from the parameter string (post body)
195 if http_method == 'POST' and postdata is not None:
196 parameters = OAuthRequest._split_url_string(postdata)
197
198 # from the url string
199 elif http_method == 'GET':
200 param_str = urlparse.urlparse(http_url).query
201 parameters = OAuthRequest._split_url_string(param_str)
202
203 if parameters:
204 return OAuthRequest(http_method, http_url, parameters)
205
206 raise OAuthError('Missing all OAuth parameters. OAuth parameters must be in the headers, post body, or url.')
207
208 @staticmethod
209 def from_consumer_and_token(oauth_consumer, token=None, http_method=HTTP_METHOD, http_url=None, parameters=None):
210 if not parameters:
211 parameters = {}
212
213 defaults = {
214 'oauth_consumer_key': oauth_consumer.key,
215 'oauth_timestamp': generate_timestamp(),
216 'oauth_nonce': generate_nonce(),
217 'oauth_version': OAuthRequest.version,
218 }
219
220 defaults.update(parameters)
221 parameters = defaults
222
223 if token:
224 parameters['oauth_token'] = token.key
225
226 return OAuthRequest(http_method, http_url, parameters)
227
228 @staticmethod
229 def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD, http_url=None, parameters=None):
230 if not parameters:
231 parameters = {}
232
233 parameters['oauth_token'] = token.key
234
235 if callback:
236 parameters['oauth_callback'] = escape(callback)
237
238 return OAuthRequest(http_method, http_url, parameters)
239
240 # util function: turn Authorization: header into parameters, has to do some unescaping
241 @staticmethod
242 def _split_header(header):
243 params = {}
244 parts = header.split(',')
245 for param in parts:
246 # ignore realm parameter
247 if param.find('OAuth realm') > -1:
248 continue
249 # remove whitespace
250 param = param.strip()
251 # split key-value
252 param_parts = param.split('=', 1)
253 # remove quotes and unescape the value
254 params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
255 return params
256
257 # util function: turn url string into parameters, has to do some unescaping
258 @staticmethod
259 def _split_url_string(param_str):
260 parameters = cgi.parse_qs(param_str, keep_blank_values=False)
261 for k, v in parameters.iteritems():
262 parameters[k] = urllib.unquote(v[0])
263 return parameters
264
265# OAuthServer is a worker to check a requests validity against a data store
266class OAuthServer(object):
267 timestamp_threshold = 300 # in seconds, five minutes
268 version = VERSION
269 signature_methods = None
270 data_store = None
271
272 def __init__(self, data_store=None, signature_methods=None):
273 self.data_store = data_store
274 self.signature_methods = signature_methods or {}
275
276 def set_data_store(self, oauth_data_store):
277 self.data_store = data_store
278
279 def get_data_store(self):
280 return self.data_store
281
282 def add_signature_method(self, signature_method):
283 self.signature_methods[signature_method.get_name()] = signature_method
284 return self.signature_methods
285
286 # process a request_token request
287 # returns the request token on success
288 def fetch_request_token(self, oauth_request):
289 try:
290 # get the request token for authorization
291 token = self._get_token(oauth_request, 'request')
292 except:
293 # no token required for the initial token request
294 version = self._get_version(oauth_request)
295 consumer = self._get_consumer(oauth_request)
296 self._check_signature(oauth_request, consumer, None)
297 # fetch a new token
298 token = self.data_store.fetch_request_token(consumer)
299 return token
300
301 # process an access_token request
302 # returns the access token on success
303 def fetch_access_token(self, oauth_request):
304 version = self._get_version(oauth_request)
305 consumer = self._get_consumer(oauth_request)
306 # get the request token
307 token = self._get_token(oauth_request, 'request')
308 self._check_signature(oauth_request, consumer, token)
309 new_token = self.data_store.fetch_access_token(consumer, token)
310 return new_token
311
312 # verify an api call, checks all the parameters
313 def verify_request(self, oauth_request):
314 # -> consumer and token
315 version = self._get_version(oauth_request)
316 consumer = self._get_consumer(oauth_request)
317 # get the access token
318 token = self._get_token(oauth_request, 'access')
319 self._check_signature(oauth_request, consumer, token)
320 parameters = oauth_request.get_nonoauth_parameters()
321 return consumer, token, parameters
322
323 # authorize a request token
324 def authorize_token(self, token, user):
325 return self.data_store.authorize_request_token(token, user)
326
327 # get the callback url
328 def get_callback(self, oauth_request):
329 return oauth_request.get_parameter('oauth_callback')
330
331 # optional support for the authenticate header
332 def build_authenticate_header(self, realm=''):
333 return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
334
335 # verify the correct version request for this server
336 def _get_version(self, oauth_request):
337 try:
338 version = oauth_request.get_parameter('oauth_version')
339 except:
340 version = VERSION
341 if version and version != self.version:
342 raise OAuthError('OAuth version %s not supported.' % str(version))
343 return version
344
345 # figure out the signature with some defaults
346 def _get_signature_method(self, oauth_request):
347 try:
348 signature_method = oauth_request.get_parameter('oauth_signature_method')
349 except:
350 signature_method = SIGNATURE_METHOD
351 try:
352 # get the signature method object
353 signature_method = self.signature_methods[signature_method]
354 except:
355 signature_method_names = ', '.join(self.signature_methods.keys())
356 raise OAuthError('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names))
357
358 return signature_method
359
360 def _get_consumer(self, oauth_request):
361 consumer_key = oauth_request.get_parameter('oauth_consumer_key')
362 if not consumer_key:
363 raise OAuthError('Invalid consumer key.')
364 consumer = self.data_store.lookup_consumer(consumer_key)
365 if not consumer:
366 raise OAuthError('Invalid consumer.')
367 return consumer
368
369 # try to find the token for the provided request token key
370 def _get_token(self, oauth_request, token_type='access'):
371 token_field = oauth_request.get_parameter('oauth_token')
372 token = self.data_store.lookup_token(token_type, token_field)
373 if not token:
374 raise OAuthError('Invalid %s token: %s' % (token_type, token_field))
375 return token
376
377 def _check_signature(self, oauth_request, consumer, token):
378 timestamp, nonce = oauth_request._get_timestamp_nonce()
379 self._check_timestamp(timestamp)
380 self._check_nonce(consumer, token, nonce)
381 signature_method = self._get_signature_method(oauth_request)
382 try:
383 signature = oauth_request.get_parameter('oauth_signature')
384 except:
385 raise OAuthError('Missing signature.')
386 # validate the signature
387 valid_sig = signature_method.check_signature(oauth_request, consumer, token, signature)
388 if not valid_sig:
389 key, base = signature_method.build_signature_base_string(oauth_request, consumer, token)
390 raise OAuthError('Invalid signature. Expected signature base string: %s' % base)
391 built = signature_method.build_signature(oauth_request, consumer, token)
392
393 def _check_timestamp(self, timestamp):
394 # verify that timestamp is recentish
395 timestamp = int(timestamp)
396 now = int(time.time())
397 lapsed = now - timestamp
398 if lapsed > self.timestamp_threshold:
399 raise OAuthError('Expired timestamp: given %d and now %s has a greater difference than threshold %d' % (timestamp, now, self.timestamp_threshold))
400
401 def _check_nonce(self, consumer, token, nonce):
402 # verify that the nonce is uniqueish
403 nonce = self.data_store.lookup_nonce(consumer, token, nonce)
404 if nonce:
405 raise OAuthError('Nonce already used: %s' % str(nonce))
406
407# OAuthClient is a worker to attempt to execute a request
408class OAuthClient(object):
409 consumer = None
410 token = None
411
412 def __init__(self, oauth_consumer, oauth_token):
413 self.consumer = oauth_consumer
414 self.token = oauth_token
415
416 def get_consumer(self):
417 return self.consumer
418
419 def get_token(self):
420 return self.token
421
422 def fetch_request_token(self, oauth_request):
423 # -> OAuthToken
424 raise NotImplementedError
425
426 def fetch_access_token(self, oauth_request):
427 # -> OAuthToken
428 raise NotImplementedError
429
430 def access_resource(self, oauth_request):
431 # -> some protected resource
432 raise NotImplementedError
433
434# OAuthDataStore is a database abstraction used to lookup consumers and tokens
435class OAuthDataStore(object):
436
437 def lookup_consumer(self, key):
438 # -> OAuthConsumer
439 raise NotImplementedError
440
441 def lookup_token(self, oauth_consumer, token_type, token_token):
442 # -> OAuthToken
443 raise NotImplementedError
444
445 def lookup_nonce(self, oauth_consumer, oauth_token, nonce, timestamp):
446 # -> OAuthToken
447 raise NotImplementedError
448
449 def fetch_request_token(self, oauth_consumer):
450 # -> OAuthToken
451 raise NotImplementedError
452
453 def fetch_access_token(self, oauth_consumer, oauth_token):
454 # -> OAuthToken
455 raise NotImplementedError
456
457 def authorize_request_token(self, oauth_token, user):
458 # -> OAuthToken
459 raise NotImplementedError
460
461# OAuthSignatureMethod is a strategy class that implements a signature method
462class OAuthSignatureMethod(object):
463 def get_name(self):
464 # -> str
465 raise NotImplementedError
466
467 def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token):
468 # -> str key, str raw
469 raise NotImplementedError
470
471 def build_signature(self, oauth_request, oauth_consumer, oauth_token):
472 # -> str
473 raise NotImplementedError
474
475 def check_signature(self, oauth_request, consumer, token, signature):
476 built = self.build_signature(oauth_request, consumer, token)
477 return built == signature
478
479class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod):
480
481 def get_name(self):
482 return 'HMAC-SHA1'
483
484 def build_signature_base_string(self, oauth_request, consumer, token):
485 sig = (
486 escape(oauth_request.get_normalized_http_method()),
487 escape(oauth_request.get_normalized_http_url()),
488 escape(oauth_request.get_normalized_parameters()),
489 )
490
491 key = '%s&' % escape(consumer.secret)
492 if token:
493 key += escape(token.secret)
494 raw = '&'.join(sig)
495 return key, raw
496
497 def build_signature(self, oauth_request, consumer, token):
498 # build the base signature string
499 key, raw = self.build_signature_base_string(oauth_request, consumer, token)
500
501 # hmac object
502 try:
503 import hashlib # 2.5
504 hashed = hmac.new(key, raw, hashlib.sha1)
505 except:
506 import sha # deprecated
507 hashed = hmac.new(key, raw, sha)
508
509 # calculate the digest base 64
510 return base64.b64encode(hashed.digest())
511
512class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod):
513
514 def get_name(self):
515 return 'PLAINTEXT'
516
517 def build_signature_base_string(self, oauth_request, consumer, token):
518 # concatenate the consumer key and secret
519 sig = escape(consumer.secret)
520 if token:
521 sig = '&'.join((sig, escape(token.secret)))
522 return sig
523
524 def build_signature(self, oauth_request, consumer, token):
525 return self.build_signature_base_string(oauth_request, consumer, token)
5260
=== removed directory 'launchpadlib/_utils'
=== removed file 'launchpadlib/_utils/__init__.py'
=== removed file 'launchpadlib/_utils/uri.py'
--- launchpadlib/_utils/uri.py 2008-08-01 16:05:31 +0000
+++ launchpadlib/_utils/uri.py 1970-01-01 00:00:00 +0000
@@ -1,578 +0,0 @@
1# Copyright 2008 Canonical Ltd.
2
3# This file is part of launchpadlib.
4#
5# launchpadlib is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License as
7# published by the Free Software Foundation, either version 3 of the
8# License, or (at your option) any later version.
9#
10# launchpadlib is distributed in the hope that it will be useful, but
11# WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13# Lesser General Public License for more details.
14#
15# You should have received a copy of the GNU Lesser General Public
16# License along with launchpadlib. If not, see
17# <http://www.gnu.org/licenses/>.
18
19"""Functions for working with generic syntax URIs."""
20
21__metaclass__ = type
22__all__ = [
23 'URI',
24 'InvalidURIError',
25 'find_uris_in_text',
26 'possible_uri_re']
27
28import re
29
30
31# Default port numbers for different URI schemes
32# The registered URI schemes comes from
33# http://www.iana.org/assignments/uri-schemes.html
34# The default ports come from the relevant RFCs
35
36_default_port = {
37 # Official schemes
38 'acap': '674',
39 'dav': '80',
40 'dict': '2628',
41 'dns': '53',
42 'ftp': '21',
43 'go': '1096',
44 'gopher': '70',
45 'h323': '1720',
46 'http': '80',
47 'https': '443',
48 'imap': '143',
49 'ipp': '631',
50 'iris.beep': '702',
51 'ldap': '389',
52 'mtqp': '1038',
53 'mupdate': '3905',
54 'nfs': '2049',
55 'nntp': '119',
56 'pop': '110',
57 'rtsp': '554',
58 'sip': '5060',
59 'sips': '5061',
60 'snmp': '161',
61 'soap.beep': '605',
62 'soap.beeps': '605',
63 'telnet': '23',
64 'tftp': '69',
65 'tip': '3372',
66 'vemmi': '575',
67 'xmlrpc.beep': '602',
68 'xmlrpc.beeps': '602',
69 'z39.50r': '210',
70 'z39.50s': '210',
71
72 # Historical schemes
73 'prospero': '1525',
74 'wais': '210',
75
76 # Common but unregistered schemes
77 'bzr+http': '80',
78 'bzr+ssh': '22',
79 'irc': '6667',
80 'sftp': '22',
81 'ssh': '22',
82 'svn': '3690',
83 'svn+ssh': '22',
84 }
85
86# Regular expressions adapted from the ABNF in the RFC
87
88scheme_re = r"(?P<scheme>[a-z][-a-z0-9+.]*)"
89
90userinfo_re = r"(?P<userinfo>(?:[-a-z0-9._~!$&\'()*+,;=:]|%[0-9a-f]{2})*)"
91# The following regular expression will match some IP address style
92# host names that the RFC would not (e.g. leading zeros on the
93# components), but is signficantly simpler.
94host_re = (r"(?P<host>[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}|"
95 r"(?:[-a-z0-9._~!$&\'()*+,;=]|%[0-9a-f]{2})*|"
96 r"\[[0-9a-z:.]+\])")
97port_re = r"(?P<port>[0-9]*)"
98
99authority_re = r"(?P<authority>(?:%s@)?%s(?::%s)?)" % (
100 userinfo_re, host_re, port_re)
101
102path_abempty_re = r"(?:/(?:[-a-z0-9._~!$&\'()*+,;=:@]|%[0-9a-f]{2})*)*"
103path_noscheme_re = (r"(?:[-a-z0-9._~!$&\'()*+,;=@]|%[0-9a-f]{2})+"
104 r"(?:/(?:[-a-z0-9._~!$&\'()*+,;=:@]|%[0-9a-f]{2})*)*")
105path_rootless_re = (r"(?:[-a-z0-9._~!$&\'()*+,;=:@]|%[0-9a-f]{2})+"
106 r"(?:/(?:[-a-z0-9._~!$&\'()*+,;=:@]|%[0-9a-f]{2})*)*")
107path_absolute_re = r"/(?:%s)?" % path_rootless_re
108path_empty_re = r""
109
110hier_part_re = r"(?P<hierpart>//%s%s|%s|%s|%s)" % (
111 authority_re, path_abempty_re, path_absolute_re, path_rootless_re,
112 path_empty_re)
113
114relative_part_re = r"(?P<relativepart>//%s%s|%s|%s|%s)" % (
115 authority_re, path_abempty_re, path_absolute_re, path_noscheme_re,
116 path_empty_re)
117
118# Additionally we also permit square braces in the query portion to
119# accomodate real-world URIs.
120query_re = r"(?P<query>(?:[-a-z0-9._~!$&\'()*+,;=:@/?\[\]]|%[0-9a-f]{2})*)"
121fragment_re = r"(?P<fragment>(?:[-a-z0-9._~!$&\'()*+,;=:@/?]|%[0-9a-f]{2})*)"
122
123uri_re = r"%s:%s(?:\?%s)?(?:#%s)?$" % (
124 scheme_re, hier_part_re, query_re, fragment_re)
125
126relative_ref_re = r"%s(?:\?%s)?(?:#%s)?$" % (
127 relative_part_re, query_re, fragment_re)
128
129uri_pat = re.compile(uri_re, re.IGNORECASE)
130relative_ref_pat = re.compile(relative_ref_re, re.IGNORECASE)
131
132
133def merge(basepath, relpath, has_authority):
134 """Merge two URI path components into a single path component.
135
136 Follows rules specified in Section 5.2.3 of RFC 3986.
137
138 The algorithm in the RFC treats the empty basepath edge case
139 differently for URIs with and without an authority section, which
140 is why the third argument is necessary.
141 """
142 if has_authority and basepath == '':
143 return '/' + relpath
144 slash = basepath.rfind('/')
145 return basepath[:slash+1] + relpath
146
147
148def remove_dot_segments(path):
149 """Remove '.' and '..' segments from a URI path.
150
151 Follows the rules specified in Section 5.2.4 of RFC 3986.
152 """
153 output = []
154 while path:
155 if path.startswith('../'):
156 path = path[3:]
157 elif path.startswith('./'):
158 path = path[2:]
159 elif path.startswith('/./') or path == '/.':
160 path = '/' + path[3:]
161 elif path.startswith('/../') or path == '/..':
162 path = '/' + path[4:]
163 if len(output) > 0:
164 del output[-1]
165 elif path in ['.', '..']:
166 path = ''
167 else:
168 if path.startswith('/'):
169 slash = path.find('/', 1)
170 else:
171 slash = path.find('/')
172 if slash < 0:
173 slash = len(path)
174 output.append(path[:slash])
175 path = path[slash:]
176 return ''.join(output)
177
178
179def normalise_unreserved(string):
180 """Return a version of 's' where no unreserved characters are encoded.
181
182 Unreserved characters are defined in Section 2.3 of RFC 3986.
183
184 Percent encoded sequences are normalised to upper case.
185 """
186 result = string.split('%')
187 unreserved = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
188 'abcdefghijklmnopqrstuvwxyz'
189 '0123456789-._~')
190 for index, item in enumerate(result):
191 if index == 0:
192 continue
193 try:
194 ch = int(item[:2], 16)
195 except ValueError:
196 continue
197 if chr(ch) in unreserved:
198 result[index] = chr(ch) + item[2:]
199 else:
200 result[index] = '%%%02X%s' % (ch, item[2:])
201 return ''.join(result)
202
203
204class InvalidURIError(Exception):
205 """Invalid URI"""
206
207
208class URI:
209 """A class that represents a URI.
210
211 This class can represent arbitrary URIs that conform to the
212 generic syntax described in RFC 3986.
213 """
214
215 def __init__(self, uri=None, scheme=None, userinfo=None, host=None,
216 port=None, path=None, query=None, fragment=None):
217 """Create a URI instance.
218
219 Can be called with either a string URI or the component parts
220 of the URI as keyword arguments.
221
222 In either case, all arguments are expected to be appropriately
223 URI encoded.
224 """
225 assert (uri is not None and scheme is None and userinfo is None and
226 host is None and port is None and path is None and
227 query is None and fragment is None) or uri is None, (
228 "URI() must be called with a single string argument or "
229 "with URI components given as keyword arguments.")
230
231 if uri is not None:
232 if isinstance(uri, unicode):
233 try:
234 uri = uri.encode('ASCII')
235 except UnicodeEncodeError:
236 raise InvalidURIError(
237 'URIs must consist of ASCII characters')
238 match = uri_pat.match(uri)
239 if match is None:
240 raise InvalidURIError('"%s" is not a valid URI' % uri)
241 self.scheme = match.group('scheme')
242 self.userinfo = match.group('userinfo')
243 self.host = match.group('host')
244 self.port = match.group('port')
245 hierpart = match.group('hierpart')
246 authority = match.group('authority')
247 if authority is None:
248 self.path = hierpart
249 else:
250 # Skip past the //authority part
251 self.path = hierpart[2+len(authority):]
252 self.query = match.group('query')
253 self.fragment = match.group('fragment')
254 else:
255 if scheme is None:
256 raise InvalidURIError('URIs must have a scheme')
257 if host is None and (userinfo is not None or port is not None):
258 raise InvalidURIError(
259 'host must be given if userinfo or port are')
260 if path is None:
261 raise InvalidURIError('URIs must have a path')
262 self.scheme = scheme
263 self.userinfo = userinfo
264 self.host = host
265 self.port = port
266 self.path = path
267 self.query = query
268 self.fragment = fragment
269
270 self._normalise()
271
272 if (self.scheme in ['http', 'https', 'ftp', 'gopher', 'telnet',
273 'imap', 'mms', 'rtsp', 'svn', 'svn+ssh',
274 'bzr', 'bzr+http', 'bzr+ssh'] and
275 not self.host):
276 raise InvalidURIError('%s URIs must have a host name' %
277 self.scheme)
278
279 def _normalise(self):
280 """Perform normalisation of URI components."""
281 self.scheme = self.scheme.lower()
282
283 if self.userinfo is not None:
284 self.userinfo = normalise_unreserved(self.userinfo)
285 if self.host is not None:
286 self.host = normalise_unreserved(self.host.lower())
287 if self.port == '':
288 self.port = None
289 elif self.port is not None:
290 if self.port == _default_port.get(self.scheme):
291 self.port = None
292 if self.host is not None and self.path == '':
293 self.path = '/'
294 self.path = normalise_unreserved(remove_dot_segments(self.path))
295
296 if self.query is not None:
297 self.query = normalise_unreserved(self.query)
298 if self.fragment is not None:
299 self.fragment = normalise_unreserved(self.fragment)
300
301 @property
302 def authority(self):
303 """The authority part of the URI"""
304 if self.host is None:
305 return None
306 authority = self.host
307 if self.userinfo is not None:
308 authority = '%s@%s' % (self.userinfo, authority)
309 if self.port is not None:
310 authority = '%s:%s' % (authority, self.port)
311 return authority
312
313 @property
314 def hier_part(self):
315 """The hierarchical part of the URI"""
316 authority = self.authority
317 if authority is None:
318 return self.path
319 else:
320 return '//%s%s' % (authority, self.path)
321
322 def __str__(self):
323 uri = '%s:%s' % (self.scheme, self.hier_part)
324 if self.query is not None:
325 uri += '?%s' % self.query
326 if self.fragment is not None:
327 uri += '#%s' % self.fragment
328 return uri
329
330 def __repr__(self):
331 return '%s(%r)' % (self.__class__.__name__, str(self))
332
333 def __eq__(self, other):
334 if isinstance(other, self.__class__):
335 return (self.scheme == other.scheme and
336 self.authority == other.authority and
337 self.path == other.path and
338 self.query == other.query and
339 self.fragment == other.fragment)
340 else:
341 return NotImplemented
342
343 def __ne__(self, other):
344 equal = self.__eq__(other)
345 if equal == NotImplemented:
346 return NotImplemented
347 else:
348 return not equal
349
350 def replace(self, **parts):
351 """Replace one or more parts of the URI, returning the result."""
352 if not parts:
353 return self
354 baseparts = dict(
355 scheme=self.scheme,
356 userinfo=self.userinfo,
357 host=self.host,
358 port=self.port,
359 path=self.path,
360 query=self.query,
361 fragment=self.fragment)
362 baseparts.update(parts)
363 return self.__class__(**baseparts)
364
365 def resolve(self, reference):
366 """Resolve the given URI reference relative to this URI.
367
368 Uses the rules from Section 5.2 of RFC 3986 to resolve the new
369 URI.
370 """
371 # If the reference is a full URI, then return it as is.
372 try:
373 return self.__class__(reference)
374 except InvalidURIError:
375 pass
376
377 match = relative_ref_pat.match(reference)
378 if match is None:
379 raise InvalidURIError("Invalid relative reference")
380
381 parts = dict(scheme=self.scheme)
382 authority = match.group('authority')
383 if authority is not None:
384 parts['userinfo'] = match.group('userinfo')
385 parts['host'] = match.group('host')
386 parts['port'] = match.group('port')
387 # Skip over the //authority part
388 parts['path'] = remove_dot_segments(
389 match.group('relativepart')[2+len(authority):])
390 parts['query'] = match.group('query')
391 else:
392 path = match.group('relativepart')
393 query = match.group('query')
394 if path == '':
395 parts['path'] = self.path
396 if query is not None:
397 parts['query'] = query
398 else:
399 parts['query'] = self.query
400 else:
401 if path.startswith('/'):
402 parts['path'] = remove_dot_segments(path)
403 else:
404 parts['path'] = merge(self.path, path,
405 has_authority=self.host is not None)
406 parts['path'] = remove_dot_segments(parts['path'])
407 parts['query'] = query
408 parts['userinfo'] = self.userinfo
409 parts['host'] = self.host
410 parts['port'] = self.port
411 parts['fragment'] = match.group('fragment')
412
413 return self.__class__(**parts)
414
415 def append(self, path):
416 """Append the given path to this URI.
417
418 The path must not start with a slash, but a slash is added to
419 base URI (before appending the path), in case it doesn't end
420 with a slash.
421 """
422 assert not path.startswith('/')
423 return self.ensureSlash().resolve(path)
424
425 def contains(self, other):
426 """Returns True if the URI 'other' is contained by this one."""
427 if (self.scheme != other.scheme or
428 self.authority != other.authority):
429 return False
430 if self.path == other.path:
431 return True
432 basepath = self.path
433 if not basepath.endswith('/'):
434 basepath += '/'
435 otherpath = other.path
436 if not otherpath.endswith('/'):
437 otherpath += '/'
438 return otherpath.startswith(basepath)
439
440 def underDomain(self, domain):
441 """Return True if the given domain name a parent of the URL's host."""
442 if len(domain) == 0:
443 return True
444 our_segments = self.host.split('.')
445 domain_segments = domain.split('.')
446 return our_segments[-len(domain_segments):] == domain_segments
447
448 def ensureSlash(self):
449 """Return a URI with the path normalised to end with a slash."""
450 if self.path.endswith('/'):
451 return self
452 else:
453 return self.replace(path=self.path + '/')
454
455 def ensureNoSlash(self):
456 """Return a URI with the path normalised to not end with a slash."""
457 if self.path.endswith('/'):
458 return self.replace(path=self.path.rstrip('/'))
459 else:
460 return self
461
462
463# Regular expression for finding URIs in a body of text:
464#
465# From RFC 3986 ABNF for URIs:
466#
467# URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
468# hier-part = "//" authority path-abempty
469# / path-absolute
470# / path-rootless
471# / path-empty
472#
473# authority = [ userinfo "@" ] host [ ":" port ]
474# userinfo = *( unreserved / pct-encoded / sub-delims / ":" )
475# host = IP-literal / IPv4address / reg-name
476# reg-name = *( unreserved / pct-encoded / sub-delims )
477# port = *DIGIT
478#
479# path-abempty = *( "/" segment )
480# path-absolute = "/" [ segment-nz *( "/" segment ) ]
481# path-rootless = segment-nz *( "/" segment )
482# path-empty = 0<pchar>
483#
484# segment = *pchar
485# segment-nz = 1*pchar
486# pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
487#
488# query = *( pchar / "/" / "?" )
489# fragment = *( pchar / "/" / "?" )
490#
491# unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
492# pct-encoded = "%" HEXDIG HEXDIG
493# sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
494# / "*" / "+" / "," / ";" / "="
495#
496# We only match a set of known scheme names. We don't handle
497# IP-literal either.
498#
499# We will simplify "unreserved / pct-encoded / sub-delims" as the
500# following regular expression:
501# [-a-zA-Z0-9._~%!$&'()*+,;=]
502#
503# We also require that the path-rootless form not begin with a
504# colon to avoid matching strings like "http::foo" (to avoid bug
505# #40255).
506#
507# The path-empty pattern is not matched either, due to false
508# positives.
509#
510# Some allowed URI punctuation characters will be trimmed if they
511# appear at the end of the URI since they may be incidental in the
512# flow of the text.
513#
514# apport has at one time produced query strings containing sqaure
515# braces (that are not percent-encoded). In RFC 2986 they seem to be
516# allowed by section 2.2 "Reserved Characters", yet section 3.4
517# "Query" appears to provide a strict definition of the query string
518# that would forbid square braces. Either way, links with
519# non-percent-encoded square braces are being used on Launchpad so
520# it's probably best to accomodate them.
521
522possible_uri_re = r'''
523\b
524(?:about|gopher|http|https|sftp|news|ftp|mailto|file|irc|jabber|xmpp)
525:
526(?:
527 (?:
528 # "//" authority path-abempty
529 //
530 (?: # userinfo
531 [%(unreserved)s:]*
532 @
533 )?
534 (?: # host
535 \d+\.\d+\.\d+\.\d+ |
536 [%(unreserved)s]*
537 )
538 (?: # port
539 : \d*
540 )?
541 (?: / [%(unreserved)s:@]* )*
542 ) | (?:
543 # path-absolute
544 /
545 (?: [%(unreserved)s:@]+
546 (?: / [%(unreserved)s:@]* )* )?
547 ) | (?:
548 # path-rootless
549 [%(unreserved)s@]
550 [%(unreserved)s:@]*
551 (?: / [%(unreserved)s:@]* )*
552 )
553)
554(?: # query
555 \?
556 [%(unreserved)s:@/\?\[\]]*
557)?
558(?: # fragment
559 \#
560 [%(unreserved)s:@/\?]*
561)?
562''' % {'unreserved': "-a-zA-Z0-9._~%!$&'()*+,;="}
563
564possible_uri_pat = re.compile(possible_uri_re, re.IGNORECASE | re.VERBOSE)
565uri_trailers_pat = re.compile(r'([,.?:);>]+)$')
566
567def find_uris_in_text(text):
568 """Scan a block of text for URIs, and yield the ones found."""
569 for match in possible_uri_pat.finditer(text):
570 uri_string = match.group()
571 # remove characters from end of URI that are not likely to be
572 # part of the URI.
573 uri_string = uri_trailers_pat.sub('', uri_string)
574 try:
575 uri = URI(uri_string)
576 except InvalidURIError:
577 continue
578 yield uri
5790
=== modified file 'setup.py'
--- setup.py 2008-10-07 15:59:44 +0000
+++ setup.py 2009-03-19 18:27:46 +0000
@@ -1,6 +1,7 @@
1#!/usr/bin/env python1#!/usr/bin/env python
2# Copyright 2008 Canonical Ltd.
32
3# Copyright 2008-2009 Canonical Ltd.
4#
4# This file is part of launchpadlib.5# This file is part of launchpadlib.
5#6#
6# launchpadlib is free software: you can redistribute it and/or modify7# launchpadlib is free software: you can redistribute it and/or modify
@@ -22,31 +23,58 @@
22import ez_setup23import ez_setup
23ez_setup.use_setuptools()24ez_setup.use_setuptools()
2425
26import sys
25from setuptools import setup, find_packages27from setuptools import setup, find_packages
28
29# generic helpers primarily for the long_description
30def generate(*docname_or_string):
31 res = []
32 for value in docname_or_string:
33 if value.endswith('.txt'):
34 f = open(value)
35 value = f.read()
36 f.close()
37 res.append(value)
38 if not value.endswith('\n'):
39 res.append('')
40 return '\n'.join(res)
41# end generic helpers
42
43sys.path.insert(0, 'src')
26from launchpadlib import __version__44from launchpadlib import __version__
2745
28
29setup(46setup(
30 name = 'launchpadlib',47 name='launchpadlib',
31 version = __version__,48 version=__version__,
32 description = """\49 packages=find_packages('src'),
33launchpadlib is a client-side library for scripting Launchpad through its web50 package_dir={'':'src'},
34services interface. Launchpad <http://launchpad.net> is a a free software51 include_package_data=True,
35hosting and development website, making it easy to collaborate across multiple52 zip_safe=False,
36projects.""",53 author='The Launchpad developers',
37 author = 'The Launchpad developers',54 author_email='launchpadlib@lists.launchpad.net',
38 author_email= 'launchpadlib@lists.launchpad.net',55 maintainer='LAZR Developers',
39 url = 'https://help.launchpad.net/API/launchpadlib',56 maintainer_email='lazr-developers@lists.launchpad.net',
40 license = 'GNU LGPLv3 or later',
41 download_url= 'https://launchpad.net/launchpadlib/+download',57 download_url= 'https://launchpad.net/launchpadlib/+download',
42 packages = find_packages(),58 description=open('README.txt').readline().strip(),
43 include_package_data = True,59 long_description=generate(
44 zip_safe = True,60 'src/launchpadlib/README.txt',
45 install_requires = [61 'src/launchpadlib/NEWS.txt'),
62 license='LGPL v3',
63 install_requires=[
46 'httplib2',64 'httplib2',
65 'lazr.uri',
66 'oauth',
67 'setuptools',
47 'simplejson',68 'simplejson',
69 'wadllib',
48 ],70 ],
49 setup_requires = [71 url='https://help.launchpad.net/API/launchpadlib',
50 'setuptools_bzr',72 classifiers=[
51 ]73 "Development Status :: 5 - Production/Stable",
74 "Intended Audience :: Developers",
75 "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)",
76 "Operating System :: OS Independent",
77 "Programming Language :: Python"],
78 setup_requires=['eggtestinfo', 'setuptools_bzr'],
79 # test_suite='launchpadlib.tests.test_suite',
52 )80 )
5381
=== added directory 'src'
=== renamed directory 'launchpadlib' => 'src/launchpadlib'
=== added file 'src/launchpadlib/NEWS.txt'
--- 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 @@
1=====================
2NEWS for launchpadlib
3=====================
4
51.0 (200X-XX-XX)
6================
7
8- Initial release
09
=== added file 'src/launchpadlib/README.txt'
--- src/launchpadlib/README.txt 1970-01-01 00:00:00 +0000
+++ src/launchpadlib/README.txt 2009-03-16 21:02:52 +0000
@@ -0,0 +1,20 @@
1..
2 This file is part of launchpadlib.
3
4 launchpadlib is free software: you can redistribute it and/or modify it
5 under the terms of the GNU Lesser General Public License as published by
6 the Free Software Foundation, either version 3 of the License, or (at your
7 option) any later version.
8
9 launchpadlib is distributed in the hope that it will be useful, but
10 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11 or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
12 License for more details.
13
14 You should have received a copy of the GNU Lesser General Public License
15 along with launchpadlib. If not, see <http://www.gnu.org/licenses/>.
16
17launchpadlib
18************
19
20See https://help.launchpad.net/API/launchpadlib .
021
=== modified file 'src/launchpadlib/_browser.py'
--- launchpadlib/_browser.py 2009-02-17 15:42:25 +0000
+++ src/launchpadlib/_browser.py 2009-03-19 15:22:56 +0000
@@ -29,22 +29,22 @@
2929
3030
31import atexit31import atexit
32from cStringIO import StringIO
32import gzip33import gzip
33import shutil
34import tempfile
35from httplib2 import (34from httplib2 import (
36 FailedToDecompressContent, FileCache, Http, safename, urlnorm)35 FailedToDecompressContent, FileCache, Http, safename, urlnorm)
36from lazr.uri import URI
37from oauth.oauth import (
38 OAuthRequest, OAuthSignatureMethod_PLAINTEXT)
39import shutil
37import simplejson40import simplejson
38from cStringIO import StringIO41import tempfile
39import zlib
40
41from urllib import urlencode42from urllib import urlencode
42from wadllib.application import Application43from wadllib.application import Application
44import zlib
45
43from launchpadlib.errors import HTTPError46from launchpadlib.errors import HTTPError
44from launchpadlib._oauth.oauth import (47from launchpadlib._json import DatetimeJSONEncoder
45 OAuthRequest, OAuthSignatureMethod_PLAINTEXT)
46from launchpadlib._utils import uri, json
47from launchpadlib._utils.json import DatetimeJSONEncoder
4848
4949
50OAUTH_REALM = 'https://api.launchpad.net'50OAUTH_REALM = 'https://api.launchpad.net'
@@ -214,7 +214,7 @@
214214
215 def get(self, resource_or_uri, headers=None, return_response=False):215 def get(self, resource_or_uri, headers=None, return_response=False):
216 """GET a representation of the given resource or URI."""216 """GET a representation of the given resource or URI."""
217 if isinstance(resource_or_uri, (basestring, uri.URI)):217 if isinstance(resource_or_uri, (basestring, URI)):
218 url = resource_or_uri218 url = resource_or_uri
219 else:219 else:
220 method = resource_or_uri.get_method('get')220 method = resource_or_uri.get_method('get')
@@ -262,5 +262,6 @@
262 headers['If-Match'] = cached_etag262 headers['If-Match'] = cached_etag
263263
264 return self._request(264 return self._request(
265 url, simplejson.dumps(representation, cls=DatetimeJSONEncoder),265 url, simplejson.dumps(representation,
266 cls=DatetimeJSONEncoder),
266 'PATCH', extra_headers=extra_headers)267 'PATCH', extra_headers=extra_headers)
267268
=== renamed file 'launchpadlib/_utils/json.py' => 'src/launchpadlib/_json.py'
=== modified file 'src/launchpadlib/credentials.py'
--- launchpadlib/credentials.py 2008-08-01 17:30:37 +0000
+++ src/launchpadlib/credentials.py 2009-03-19 15:22:56 +0000
@@ -25,13 +25,13 @@
25 'Credentials',25 'Credentials',
26 ]26 ]
2727
28from ConfigParser import SafeConfigParser
28import cgi29import cgi
29import httplib230import httplib2
31from oauth.oauth import OAuthConsumer, OAuthToken
30from urllib import urlencode32from urllib import urlencode
3133
32from ConfigParser import SafeConfigParser
33from launchpadlib.errors import CredentialsFileError, HTTPError34from launchpadlib.errors import CredentialsFileError, HTTPError
34from launchpadlib._oauth.oauth import OAuthConsumer, OAuthToken
3535
3636
37CREDENTIALS_FILE_VERSION = '1'37CREDENTIALS_FILE_VERSION = '1'
3838
=== modified file 'src/launchpadlib/launchpad.py'
--- launchpadlib/launchpad.py 2008-08-22 14:03:54 +0000
+++ src/launchpadlib/launchpad.py 2009-03-16 21:02:52 +0000
@@ -28,9 +28,9 @@
28import webbrowser28import webbrowser
2929
30from wadllib.application import Resource as WadlResource30from wadllib.application import Resource as WadlResource
31from lazr.uri import URI
3132
32from launchpadlib._browser import Browser33from launchpadlib._browser import Browser
33from launchpadlib._utils.uri import URI
34from launchpadlib.resource import Resource34from launchpadlib.resource import Resource
35from launchpadlib.credentials import AccessToken, Credentials35from launchpadlib.credentials import AccessToken, Credentials
3636
3737
=== modified file 'src/launchpadlib/resource.py'
--- launchpadlib/resource.py 2009-02-17 15:42:25 +0000
+++ src/launchpadlib/resource.py 2009-03-19 15:22:56 +0000
@@ -32,9 +32,9 @@
32from StringIO import StringIO32from StringIO import StringIO
33import urllib33import urllib
34from urlparse import urlparse34from urlparse import urlparse
35from lazr.uri import URI
3536
36from launchpadlib._utils.uri import URI37from launchpadlib._json import DatetimeJSONEncoder
37from launchpadlib._utils.json import DatetimeJSONEncoder
38from launchpadlib.errors import HTTPError38from launchpadlib.errors import HTTPError
39from wadllib.application import Resource as WadlResource39from wadllib.application import Resource as WadlResource
4040

Subscribers

People subscribed via source and target branches