~ubuntu-branches/ubuntu/precise/desktopcouch/precise

« back to all changes in this revision

Viewing changes to desktopcouch/records/http.py

  • Committer: Bazaar Package Importer
  • Author(s): Chad MILLER
  • Date: 2011-01-12 15:08:25 UTC
  • mfrom: (1.5.10 upstream)
  • Revision ID: james.westby@ubuntu.com-20110112150825-bzvn23kzufr0qdyb
Tags: 1.0.5-0ubuntu1
* New upstream release, skipping a few buggy releases.
* Split code into binary packages:
  - desktopcouch, configuration files and dependencies, but no code.
  - python-desktopcouch: transitional package
  - python-desktopcouch-application: local DB startup and discovery
  - python-desktopcouch-records: library for DB access anywhere
  - python-desktopcouch-recordtypes: support specific data structures
  - desktopcouch-ubuntuone, replication and pairing with cloud service
* Drop patch that some maverick apps incorrectly needed.
  patches/0-items-should-expose-private-data-for-now.patch
* Update package compatibility-version, 6 -> 7.
* Use newer debhelper and use python-support instead of python-central.
* Depend on contemporary python-couchdb, instead of ancient version.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2009 Canonical Ltd.
 
2
#
 
3
# This file is part of desktopcouch.
 
4
#
 
5
#  desktopcouch is free software: you can redistribute it and/or modify
 
6
# it under the terms of the GNU Lesser General Public License version 3
 
7
# as published by the Free Software Foundation.
 
8
#
 
9
# desktopcouch is distributed in the hope that it will be useful,
 
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
12
# GNU Lesser General Public License for more details.
 
13
#
 
14
# You should have received a copy of the GNU Lesser General Public License
 
15
# along with desktopcouch.  If not, see <http://www.gnu.org/licenses/>.
 
16
#
 
17
# Authors: Vincenzo Di Somma <vincenzo.di.somma@canonical.com>
 
18
 
 
19
"""This modules olds some code that should back ported to python-couchdb"""
 
20
 
 
21
import cgi
 
22
import errno
 
23
import re
 
24
import socket
 
25
import sys
 
26
import time
 
27
import urlparse
 
28
 
 
29
from httplib import BadStatusLine
 
30
from urlparse import urlsplit, urlunsplit
 
31
 
 
32
from oauth import oauth
 
33
 
 
34
# pylint can't deal with failing imports even when they're handled
 
35
# pylint: disable=F0401
 
36
try:
 
37
    from cStringIO import StringIO
 
38
except ImportError:
 
39
    from StringIO import StringIO
 
40
 
 
41
# pylint: enable=F0401
 
42
 
 
43
from couchdb.http import (
 
44
    Session, CHUNK_SIZE, CACHE_SIZE, RedirectLimit, ResponseBody, Unauthorized,
 
45
    PreconditionFailed, ServerError, ResourceNotFound, ResourceConflict)
 
46
 
 
47
from couchdb import json as couchdbjson
 
48
 
 
49
NORMALIZE_SPACE = re.compile(r'(?:\r\n)?[ \t]+')
 
50
 
 
51
 
 
52
class OAuthSession(Session):
 
53
    """Session that can handle OAuth"""
 
54
 
 
55
    def __init__(self, cache=None, timeout=None, max_redirects=5,
 
56
                 credentials=None):
 
57
        """Initialize an HTTP client session with oauth credential. """
 
58
        super(OAuthSession, self).__init__(cache=cache,
 
59
                                           timeout=timeout,
 
60
                                           max_redirects=max_redirects)
 
61
        self.credentials = credentials
 
62
 
 
63
    def request(self, method, url, body=None, headers=None, credentials=None,
 
64
                num_redirects=0):
 
65
 
 
66
        def normalize_headers(headers):
 
67
            """normalize the headers so oauth likes them"""
 
68
            return dict(
 
69
                [(key.lower(),
 
70
                  NORMALIZE_SPACE.sub(
 
71
                      value,
 
72
                      ' ').strip())  for (key, value) in headers.iteritems()])
 
73
 
 
74
        def oauth_sign(creds, url, method):
 
75
            """Sign the url with the tokens and return an header"""
 
76
            consumer = oauth.OAuthConsumer(creds['consumer_key'],
 
77
                                           creds['consumer_secret'])
 
78
            access_token = oauth.OAuthToken(creds['token'],
 
79
                                            creds['token_secret'])
 
80
            sig_method = oauth.OAuthSignatureMethod_HMAC_SHA1
 
81
            query = urlparse.urlparse(url)[4]
 
82
            querystr_as_dict = dict(cgi.parse_qsl(query))
 
83
            req = oauth.OAuthRequest.from_consumer_and_token(
 
84
                consumer,
 
85
                access_token,
 
86
                http_method=method,
 
87
                http_url=url,
 
88
                parameters=querystr_as_dict)
 
89
            req.sign_request(sig_method(), consumer, access_token)
 
90
            return req.to_header()
 
91
 
 
92
        if url in self.perm_redirects:
 
93
            url = self.perm_redirects[url]
 
94
        method = method.upper()
 
95
 
 
96
        if headers is None:
 
97
            headers = {}
 
98
        headers.setdefault('Accept', 'application/json')
 
99
        headers['User-Agent'] = self.user_agent
 
100
 
 
101
        cached_resp = None
 
102
        if method in ('GET', 'HEAD'):
 
103
            cached_resp = self.cache.get(url)
 
104
            if cached_resp is not None:
 
105
                etag = cached_resp[1].get('etag')
 
106
                if etag:
 
107
                    headers['If-None-Match'] = etag
 
108
 
 
109
        if body is not None:
 
110
            if not isinstance(body, basestring):
 
111
                try:
 
112
                    body = couchdbjson.encode(body).encode('utf-8')
 
113
                except TypeError:
 
114
                    pass
 
115
                else:
 
116
                    headers.setdefault('Content-Type', 'application/json')
 
117
            if isinstance(body, basestring):
 
118
                headers.setdefault('Content-Length', str(len(body)))
 
119
            else:
 
120
                headers['Transfer-Encoding'] = 'chunked'
 
121
 
 
122
        if credentials:
 
123
            creds = credentials
 
124
        elif self.credentials:
 
125
            creds = self.credentials
 
126
        else:
 
127
            creds = None
 
128
        if creds:
 
129
            headers.update(normalize_headers(
 
130
                    oauth_sign(creds, url, method)))
 
131
 
 
132
        path_query = urlunsplit(('', '') + urlsplit(url)[2:4] + ('',))
 
133
        conn = self._get_connection(url)
 
134
 
 
135
        def _try_request_with_retries(retries):
 
136
            """Retries the request if it fails for a socket problem"""
 
137
            while True:
 
138
                try:
 
139
                    return _try_request()
 
140
                except socket.error, e:
 
141
                    ecode = e.args[0]
 
142
                    if ecode not in self.retryable_errors:
 
143
                        raise
 
144
                    try:
 
145
                        delay = retries.next()
 
146
                    except StopIteration:
 
147
                        # No more retries, raise last socket error.
 
148
                        raise e
 
149
                    time.sleep(delay)
 
150
                    conn.close()
 
151
 
 
152
        def _try_request():
 
153
            """Tries the request and handle socket problems"""
 
154
            try:
 
155
                if conn.sock is None:
 
156
                    conn.connect()
 
157
                conn.putrequest(method, path_query, skip_accept_encoding=True)
 
158
                for header in headers:
 
159
                    conn.putheader(header, headers[header])
 
160
                conn.endheaders()
 
161
                if body is not None:
 
162
                    if isinstance(body, str):
 
163
                        conn.sock.sendall(body)
 
164
                    else:  # assume a file-like object and send in chunks
 
165
                        while 1:
 
166
                            chunk = body.read(CHUNK_SIZE)
 
167
                            if not chunk:
 
168
                                break
 
169
                            conn.sock.sendall(('%x\r\n' % len(chunk)) +
 
170
                                              chunk + '\r\n')
 
171
                        conn.sock.sendall('0\r\n\r\n')
 
172
                return conn.getresponse()
 
173
            except BadStatusLine, e:
 
174
                # httplib raises a BadStatusLine when it cannot read the status
 
175
                # line saying, "Presumably, the server closed the connection
 
176
                # before sending a valid response."
 
177
                # Raise as ECONNRESET to simplify retry logic.
 
178
                if e.line == '' or e.line == "''":
 
179
                    raise socket.error(errno.ECONNRESET)
 
180
                else:
 
181
                    raise
 
182
 
 
183
        resp = _try_request_with_retries(iter(self.retry_delays))
 
184
        status = resp.status
 
185
 
 
186
        # Handle conditional response
 
187
        if status == 304 and method in ('GET', 'HEAD'):
 
188
            resp.read()
 
189
            self._return_connection(url, conn)
 
190
            status, msg, data = cached_resp
 
191
            if data is not None:
 
192
                data = StringIO(data)
 
193
            return status, msg, data
 
194
        elif cached_resp:
 
195
            del self.cache[url]
 
196
 
 
197
        # Handle redirects
 
198
        if status == 303 or \
 
199
                method in ('GET', 'HEAD') and status in (301, 302, 307):
 
200
            resp.read()
 
201
            self._return_connection(url, conn)
 
202
            if num_redirects > self.max_redirects:
 
203
                raise RedirectLimit('Redirection limit exceeded')
 
204
            location = resp.getheader('location')
 
205
            if status == 301:
 
206
                self.perm_redirects[url] = location
 
207
            elif status == 303:
 
208
                method = 'GET'
 
209
            return self.request(method, location, body, headers,
 
210
                                num_redirects=num_redirects + 1)
 
211
 
 
212
        data = None
 
213
        streamed = False
 
214
 
 
215
        # Read the full response for empty responses so that the connection is
 
216
        # in good state for the next request
 
217
        if method == 'HEAD' or resp.getheader('content-length') == '0' or \
 
218
                status < 200 or status in (204, 304):
 
219
            resp.read()
 
220
            self._return_connection(url, conn)
 
221
 
 
222
        # Buffer small non-JSON response bodies
 
223
        elif int(resp.getheader('content-length', sys.maxint)) < CHUNK_SIZE:
 
224
            data = resp.read()
 
225
            self._return_connection(url, conn)
 
226
 
 
227
        # For large or chunked response bodies, do not buffer the full body,
 
228
        # and instead return a minimal file-like object
 
229
        else:
 
230
            data = ResponseBody(resp,
 
231
                                lambda: self._return_connection(url, conn))
 
232
            streamed = True
 
233
 
 
234
        # Handle errors
 
235
        if status >= 400:
 
236
            if data is not None:
 
237
                data = couchdbjson.decode(data)
 
238
                # pylint: disable=E1103
 
239
                error = data.get('error'), data.get('reason')
 
240
                # pylint: enable=E1103
 
241
            elif method != 'HEAD':
 
242
                error = resp.read()
 
243
                self._return_connection(url, conn)
 
244
            else:
 
245
                error = ''
 
246
            if status == 401:
 
247
                raise Unauthorized(error)
 
248
            elif status == 404:
 
249
                raise ResourceNotFound(error)
 
250
            elif status == 409:
 
251
                raise ResourceConflict(error)
 
252
            elif status == 412:
 
253
                raise PreconditionFailed(error)
 
254
            else:
 
255
                raise ServerError((status, error))
 
256
 
 
257
        # Store cachable responses
 
258
        if not streamed and method == 'GET' and 'etag' in resp.msg:
 
259
            self.cache[url] = (status, resp.msg, data)
 
260
            if len(self.cache) > CACHE_SIZE[1]:
 
261
                self._clean_cache()
 
262
 
 
263
        if not streamed and data is not None:
 
264
            data = StringIO(data)
 
265
 
 
266
        return status, resp.msg, data