/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/transport/http/__init__.py

  • Committer: Aaron Bentley
  • Date: 2008-10-16 20:29:36 UTC
  • mfrom: (3779 +trunk)
  • mto: (0.14.24 prepare-shelf)
  • mto: This revision was merged to the branch mainline in revision 3820.
  • Revision ID: aaron@aaronbentley.com-20081016202936-q8budtnystke56fn
Merge with bzr.dev

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
"""Base implementation of Transport over http.
 
18
 
 
19
There are separate implementation modules for each http client implementation.
 
20
"""
 
21
 
 
22
from cStringIO import StringIO
 
23
import mimetools
 
24
import re
 
25
import urlparse
 
26
import urllib
 
27
import sys
 
28
import weakref
 
29
 
 
30
from bzrlib import (
 
31
    debug,
 
32
    errors,
 
33
    ui,
 
34
    urlutils,
 
35
    )
 
36
from bzrlib.smart import medium
 
37
from bzrlib.symbol_versioning import (
 
38
        deprecated_method,
 
39
        )
 
40
from bzrlib.trace import mutter
 
41
from bzrlib.transport import (
 
42
    ConnectedTransport,
 
43
    _CoalescedOffset,
 
44
    Transport,
 
45
    )
 
46
 
 
47
# TODO: This is not used anymore by HttpTransport_urllib
 
48
# (extracting the auth info and prompting the user for a password
 
49
# have been split), only the tests still use it. It should be
 
50
# deleted and the tests rewritten ASAP to stay in sync.
 
51
def extract_auth(url, password_manager):
 
52
    """Extract auth parameters from am HTTP/HTTPS url and add them to the given
 
53
    password manager.  Return the url, minus those auth parameters (which
 
54
    confuse urllib2).
 
55
    """
 
56
    if not re.match(r'^(https?)(\+\w+)?://', url):
 
57
        raise ValueError(
 
58
            'invalid absolute url %r' % (url,))
 
59
    scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
 
60
 
 
61
    if '@' in netloc:
 
62
        auth, netloc = netloc.split('@', 1)
 
63
        if ':' in auth:
 
64
            username, password = auth.split(':', 1)
 
65
        else:
 
66
            username, password = auth, None
 
67
        if ':' in netloc:
 
68
            host = netloc.split(':', 1)[0]
 
69
        else:
 
70
            host = netloc
 
71
        username = urllib.unquote(username)
 
72
        if password is not None:
 
73
            password = urllib.unquote(password)
 
74
        else:
 
75
            password = ui.ui_factory.get_password(
 
76
                prompt='HTTP %(user)s@%(host)s password',
 
77
                user=username, host=host)
 
78
        password_manager.add_password(None, host, username, password)
 
79
    url = urlparse.urlunsplit((scheme, netloc, path, query, fragment))
 
80
    return url
 
81
 
 
82
 
 
83
class HttpTransportBase(ConnectedTransport):
 
84
    """Base class for http implementations.
 
85
 
 
86
    Does URL parsing, etc, but not any network IO.
 
87
 
 
88
    The protocol can be given as e.g. http+urllib://host/ to use a particular
 
89
    implementation.
 
90
    """
 
91
 
 
92
    # _unqualified_scheme: "http" or "https"
 
93
    # _scheme: may have "+pycurl", etc
 
94
 
 
95
    def __init__(self, base, _from_transport=None):
 
96
        """Set the base path where files will be stored."""
 
97
        proto_match = re.match(r'^(https?)(\+\w+)?://', base)
 
98
        if not proto_match:
 
99
            raise AssertionError("not a http url: %r" % base)
 
100
        self._unqualified_scheme = proto_match.group(1)
 
101
        impl_name = proto_match.group(2)
 
102
        if impl_name:
 
103
            impl_name = impl_name[1:]
 
104
        self._impl_name = impl_name
 
105
        super(HttpTransportBase, self).__init__(base,
 
106
                                                _from_transport=_from_transport)
 
107
        self._medium = None
 
108
        # range hint is handled dynamically throughout the life
 
109
        # of the transport object. We start by trying multi-range
 
110
        # requests and if the server returns bogus results, we
 
111
        # retry with single range requests and, finally, we
 
112
        # forget about range if the server really can't
 
113
        # understand. Once acquired, this piece of info is
 
114
        # propagated to clones.
 
115
        if _from_transport is not None:
 
116
            self._range_hint = _from_transport._range_hint
 
117
        else:
 
118
            self._range_hint = 'multi'
 
119
 
 
120
    def has(self, relpath):
 
121
        raise NotImplementedError("has() is abstract on %r" % self)
 
122
 
 
123
    def get(self, relpath):
 
124
        """Get the file at the given relative path.
 
125
 
 
126
        :param relpath: The relative path to the file
 
127
        """
 
128
        code, response_file = self._get(relpath, None)
 
129
        # FIXME: some callers want an iterable... One step forward, three steps
 
130
        # backwards :-/ And not only an iterable, but an iterable that can be
 
131
        # seeked backwards, so we will never be able to do that.  One such
 
132
        # known client is bzrlib.bundle.serializer.v4.get_bundle_reader. At the
 
133
        # time of this writing it's even the only known client -- vila20071203
 
134
        return StringIO(response_file.read())
 
135
 
 
136
    def _get(self, relpath, ranges, tail_amount=0):
 
137
        """Get a file, or part of a file.
 
138
 
 
139
        :param relpath: Path relative to transport base URL
 
140
        :param ranges: None to get the whole file;
 
141
            or  a list of _CoalescedOffset to fetch parts of a file.
 
142
        :param tail_amount: The amount to get from the end of the file.
 
143
 
 
144
        :returns: (http_code, result_file)
 
145
        """
 
146
        raise NotImplementedError(self._get)
 
147
 
 
148
    def _remote_path(self, relpath):
 
149
        """See ConnectedTransport._remote_path.
 
150
 
 
151
        user and passwords are not embedded in the path provided to the server.
 
152
        """
 
153
        relative = urlutils.unescape(relpath).encode('utf-8')
 
154
        path = self._combine_paths(self._path, relative)
 
155
        return self._unsplit_url(self._unqualified_scheme,
 
156
                                 None, None, self._host, self._port, path)
 
157
 
 
158
    def _create_auth(self):
 
159
        """Returns a dict returning the credentials provided at build time."""
 
160
        auth = dict(host=self._host, port=self._port,
 
161
                    user=self._user, password=self._password,
 
162
                    protocol=self._unqualified_scheme,
 
163
                    path=self._path)
 
164
        return auth
 
165
 
 
166
    def get_smart_medium(self):
 
167
        """See Transport.get_smart_medium."""
 
168
        if self._medium is None:
 
169
            # Since medium holds some state (smart server probing at least), we
 
170
            # need to keep it around. Note that this is needed because medium
 
171
            # has the same 'base' attribute as the transport so it can't be
 
172
            # shared between transports having different bases.
 
173
            self._medium = SmartClientHTTPMedium(self)
 
174
        return self._medium
 
175
 
 
176
 
 
177
    def _degrade_range_hint(self, relpath, ranges, exc_info):
 
178
        if self._range_hint == 'multi':
 
179
            self._range_hint = 'single'
 
180
            mutter('Retry "%s" with single range request' % relpath)
 
181
        elif self._range_hint == 'single':
 
182
            self._range_hint = None
 
183
            mutter('Retry "%s" without ranges' % relpath)
 
184
        else:
 
185
            # We tried all the tricks, but nothing worked. We re-raise the
 
186
            # original exception; the 'mutter' calls above will indicate that
 
187
            # further tries were unsuccessful
 
188
            raise exc_info[0], exc_info[1], exc_info[2]
 
189
 
 
190
    # _coalesce_offsets is a helper for readv, it try to combine ranges without
 
191
    # degrading readv performances. _bytes_to_read_before_seek is the value
 
192
    # used for the limit parameter and has been tuned for other transports. For
 
193
    # HTTP, the name is inappropriate but the parameter is still useful and
 
194
    # helps reduce the number of chunks in the response. The overhead for a
 
195
    # chunk (headers, length, footer around the data itself is variable but
 
196
    # around 50 bytes. We use 128 to reduce the range specifiers that appear in
 
197
    # the header, some servers (notably Apache) enforce a maximum length for a
 
198
    # header and issue a '400: Bad request' error when too much ranges are
 
199
    # specified.
 
200
    _bytes_to_read_before_seek = 128
 
201
    # No limit on the offset number that get combined into one, we are trying
 
202
    # to avoid downloading the whole file.
 
203
    _max_readv_combine = 0
 
204
    # By default Apache has a limit of ~400 ranges before replying with a 400
 
205
    # Bad Request. So we go underneath that amount to be safe.
 
206
    _max_get_ranges = 200
 
207
    # We impose no limit on the range size. But see _pycurl.py for a different
 
208
    # use.
 
209
    _get_max_size = 0
 
210
 
 
211
    def _readv(self, relpath, offsets):
 
212
        """Get parts of the file at the given relative path.
 
213
 
 
214
        :param offsets: A list of (offset, size) tuples.
 
215
        :param return: A list or generator of (offset, data) tuples
 
216
        """
 
217
 
 
218
        # offsets may be a generator, we will iterate it several times, so
 
219
        # build a list
 
220
        offsets = list(offsets)
 
221
 
 
222
        try_again = True
 
223
        retried_offset = None
 
224
        while try_again:
 
225
            try_again = False
 
226
 
 
227
            # Coalesce the offsets to minimize the GET requests issued
 
228
            sorted_offsets = sorted(offsets)
 
229
            coalesced = self._coalesce_offsets(
 
230
                sorted_offsets, limit=self._max_readv_combine,
 
231
                fudge_factor=self._bytes_to_read_before_seek,
 
232
                max_size=self._get_max_size)
 
233
 
 
234
            # Turn it into a list, we will iterate it several times
 
235
            coalesced = list(coalesced)
 
236
            if 'http' in debug.debug_flags:
 
237
                mutter('http readv of %s  offsets => %s collapsed %s',
 
238
                    relpath, len(offsets), len(coalesced))
 
239
 
 
240
            # Cache the data read, but only until it's been used
 
241
            data_map = {}
 
242
            # We will iterate on the data received from the GET requests and
 
243
            # serve the corresponding offsets respecting the initial order. We
 
244
            # need an offset iterator for that.
 
245
            iter_offsets = iter(offsets)
 
246
            cur_offset_and_size = iter_offsets.next()
 
247
 
 
248
            try:
 
249
                for cur_coal, rfile in self._coalesce_readv(relpath, coalesced):
 
250
                    # Split the received chunk
 
251
                    for offset, size in cur_coal.ranges:
 
252
                        start = cur_coal.start + offset
 
253
                        rfile.seek(start, 0)
 
254
                        data = rfile.read(size)
 
255
                        data_len = len(data)
 
256
                        if data_len != size:
 
257
                            raise errors.ShortReadvError(relpath, start, size,
 
258
                                                         actual=data_len)
 
259
                        if (start, size) == cur_offset_and_size:
 
260
                            # The offset requested are sorted as the coalesced
 
261
                            # ones, no need to cache. Win !
 
262
                            yield cur_offset_and_size[0], data
 
263
                            cur_offset_and_size = iter_offsets.next()
 
264
                        else:
 
265
                            # Different sorting. We need to cache.
 
266
                            data_map[(start, size)] = data
 
267
 
 
268
                    # Yield everything we can
 
269
                    while cur_offset_and_size in data_map:
 
270
                        # Clean the cached data since we use it
 
271
                        # XXX: will break if offsets contains duplicates --
 
272
                        # vila20071129
 
273
                        this_data = data_map.pop(cur_offset_and_size)
 
274
                        yield cur_offset_and_size[0], this_data
 
275
                        cur_offset_and_size = iter_offsets.next()
 
276
 
 
277
            except (errors.ShortReadvError, errors.InvalidRange,
 
278
                    errors.InvalidHttpRange), e:
 
279
                mutter('Exception %r: %s during http._readv',e, e)
 
280
                if (not isinstance(e, errors.ShortReadvError)
 
281
                    or retried_offset == cur_offset_and_size):
 
282
                    # We don't degrade the range hint for ShortReadvError since
 
283
                    # they do not indicate a problem with the server ability to
 
284
                    # handle ranges. Except when we fail to get back a required
 
285
                    # offset twice in a row. In that case, falling back to
 
286
                    # single range or whole file should help or end up in a
 
287
                    # fatal exception.
 
288
                    self._degrade_range_hint(relpath, coalesced, sys.exc_info())
 
289
                # Some offsets may have been already processed, so we retry
 
290
                # only the unsuccessful ones.
 
291
                offsets = [cur_offset_and_size] + [o for o in iter_offsets]
 
292
                retried_offset = cur_offset_and_size
 
293
                try_again = True
 
294
 
 
295
    def _coalesce_readv(self, relpath, coalesced):
 
296
        """Issue several GET requests to satisfy the coalesced offsets"""
 
297
 
 
298
        def get_and_yield(relpath, coalesced):
 
299
            if coalesced:
 
300
                # Note that the _get below may raise
 
301
                # errors.InvalidHttpRange. It's the caller's responsibility to
 
302
                # decide how to retry since it may provide different coalesced
 
303
                # offsets.
 
304
                code, rfile = self._get(relpath, coalesced)
 
305
                for coal in coalesced:
 
306
                    yield coal, rfile
 
307
 
 
308
        if self._range_hint is None:
 
309
            # Download whole file
 
310
            for c, rfile in get_and_yield(relpath, coalesced):
 
311
                yield c, rfile
 
312
        else:
 
313
            total = len(coalesced)
 
314
            if self._range_hint == 'multi':
 
315
                max_ranges = self._max_get_ranges
 
316
            elif self._range_hint == 'single':
 
317
                max_ranges = total
 
318
            else:
 
319
                raise AssertionError("Unknown _range_hint %r"
 
320
                                     % (self._range_hint,))
 
321
            # TODO: Some web servers may ignore the range requests and return
 
322
            # the whole file, we may want to detect that and avoid further
 
323
            # requests.
 
324
            # Hint: test_readv_multiple_get_requests will fail once we do that
 
325
            cumul = 0
 
326
            ranges = []
 
327
            for coal in coalesced:
 
328
                if ((self._get_max_size > 0
 
329
                     and cumul + coal.length > self._get_max_size)
 
330
                    or len(ranges) >= max_ranges):
 
331
                    # Get that much and yield
 
332
                    for c, rfile in get_and_yield(relpath, ranges):
 
333
                        yield c, rfile
 
334
                    # Restart with the current offset
 
335
                    ranges = [coal]
 
336
                    cumul = coal.length
 
337
                else:
 
338
                    ranges.append(coal)
 
339
                    cumul += coal.length
 
340
            # Get the rest and yield
 
341
            for c, rfile in get_and_yield(relpath, ranges):
 
342
                yield c, rfile
 
343
 
 
344
    def recommended_page_size(self):
 
345
        """See Transport.recommended_page_size().
 
346
 
 
347
        For HTTP we suggest a large page size to reduce the overhead
 
348
        introduced by latency.
 
349
        """
 
350
        return 64 * 1024
 
351
 
 
352
    def _post(self, body_bytes):
 
353
        """POST body_bytes to .bzr/smart on this transport.
 
354
        
 
355
        :returns: (response code, response body file-like object).
 
356
        """
 
357
        # TODO: Requiring all the body_bytes to be available at the beginning of
 
358
        # the POST may require large client buffers.  It would be nice to have
 
359
        # an interface that allows streaming via POST when possible (and
 
360
        # degrades to a local buffer when not).
 
361
        raise NotImplementedError(self._post)
 
362
 
 
363
    def put_file(self, relpath, f, mode=None):
 
364
        """Copy the file-like object into the location.
 
365
 
 
366
        :param relpath: Location to put the contents, relative to base.
 
367
        :param f:       File-like object.
 
368
        """
 
369
        raise errors.TransportNotPossible('http PUT not supported')
 
370
 
 
371
    def mkdir(self, relpath, mode=None):
 
372
        """Create a directory at the given path."""
 
373
        raise errors.TransportNotPossible('http does not support mkdir()')
 
374
 
 
375
    def rmdir(self, relpath):
 
376
        """See Transport.rmdir."""
 
377
        raise errors.TransportNotPossible('http does not support rmdir()')
 
378
 
 
379
    def append_file(self, relpath, f, mode=None):
 
380
        """Append the text in the file-like object into the final
 
381
        location.
 
382
        """
 
383
        raise errors.TransportNotPossible('http does not support append()')
 
384
 
 
385
    def copy(self, rel_from, rel_to):
 
386
        """Copy the item at rel_from to the location at rel_to"""
 
387
        raise errors.TransportNotPossible('http does not support copy()')
 
388
 
 
389
    def copy_to(self, relpaths, other, mode=None, pb=None):
 
390
        """Copy a set of entries from self into another Transport.
 
391
 
 
392
        :param relpaths: A list/generator of entries to be copied.
 
393
 
 
394
        TODO: if other is LocalTransport, is it possible to
 
395
              do better than put(get())?
 
396
        """
 
397
        # At this point HttpTransport might be able to check and see if
 
398
        # the remote location is the same, and rather than download, and
 
399
        # then upload, it could just issue a remote copy_this command.
 
400
        if isinstance(other, HttpTransportBase):
 
401
            raise errors.TransportNotPossible(
 
402
                'http cannot be the target of copy_to()')
 
403
        else:
 
404
            return super(HttpTransportBase, self).\
 
405
                    copy_to(relpaths, other, mode=mode, pb=pb)
 
406
 
 
407
    def move(self, rel_from, rel_to):
 
408
        """Move the item at rel_from to the location at rel_to"""
 
409
        raise errors.TransportNotPossible('http does not support move()')
 
410
 
 
411
    def delete(self, relpath):
 
412
        """Delete the item at relpath"""
 
413
        raise errors.TransportNotPossible('http does not support delete()')
 
414
 
 
415
    def external_url(self):
 
416
        """See bzrlib.transport.Transport.external_url."""
 
417
        # HTTP URL's are externally usable.
 
418
        return self.base
 
419
 
 
420
    def is_readonly(self):
 
421
        """See Transport.is_readonly."""
 
422
        return True
 
423
 
 
424
    def listable(self):
 
425
        """See Transport.listable."""
 
426
        return False
 
427
 
 
428
    def stat(self, relpath):
 
429
        """Return the stat information for a file.
 
430
        """
 
431
        raise errors.TransportNotPossible('http does not support stat()')
 
432
 
 
433
    def lock_read(self, relpath):
 
434
        """Lock the given file for shared (read) access.
 
435
        :return: A lock object, which should be passed to Transport.unlock()
 
436
        """
 
437
        # The old RemoteBranch ignore lock for reading, so we will
 
438
        # continue that tradition and return a bogus lock object.
 
439
        class BogusLock(object):
 
440
            def __init__(self, path):
 
441
                self.path = path
 
442
            def unlock(self):
 
443
                pass
 
444
        return BogusLock(relpath)
 
445
 
 
446
    def lock_write(self, relpath):
 
447
        """Lock the given file for exclusive (write) access.
 
448
        WARNING: many transports do not support this, so trying avoid using it
 
449
 
 
450
        :return: A lock object, which should be passed to Transport.unlock()
 
451
        """
 
452
        raise errors.TransportNotPossible('http does not support lock_write()')
 
453
 
 
454
    def clone(self, offset=None):
 
455
        """Return a new HttpTransportBase with root at self.base + offset
 
456
 
 
457
        We leave the daughter classes take advantage of the hint
 
458
        that it's a cloning not a raw creation.
 
459
        """
 
460
        if offset is None:
 
461
            return self.__class__(self.base, self)
 
462
        else:
 
463
            return self.__class__(self.abspath(offset), self)
 
464
 
 
465
    def _attempted_range_header(self, offsets, tail_amount):
 
466
        """Prepare a HTTP Range header at a level the server should accept.
 
467
 
 
468
        :return: the range header representing offsets/tail_amount or None if
 
469
            no header can be built.
 
470
        """
 
471
 
 
472
        if self._range_hint == 'multi':
 
473
            # Generate the header describing all offsets
 
474
            return self._range_header(offsets, tail_amount)
 
475
        elif self._range_hint == 'single':
 
476
            # Combine all the requested ranges into a single
 
477
            # encompassing one
 
478
            if len(offsets) > 0:
 
479
                if tail_amount not in (0, None):
 
480
                    # Nothing we can do here to combine ranges with tail_amount
 
481
                    # in a single range, just returns None. The whole file
 
482
                    # should be downloaded.
 
483
                    return None
 
484
                else:
 
485
                    start = offsets[0].start
 
486
                    last = offsets[-1]
 
487
                    end = last.start + last.length - 1
 
488
                    whole = self._coalesce_offsets([(start, end - start + 1)],
 
489
                                                   limit=0, fudge_factor=0)
 
490
                    return self._range_header(list(whole), 0)
 
491
            else:
 
492
                # Only tail_amount, requested, leave range_header
 
493
                # do its work
 
494
                return self._range_header(offsets, tail_amount)
 
495
        else:
 
496
            return None
 
497
 
 
498
    @staticmethod
 
499
    def _range_header(ranges, tail_amount):
 
500
        """Turn a list of bytes ranges into a HTTP Range header value.
 
501
 
 
502
        :param ranges: A list of _CoalescedOffset
 
503
        :param tail_amount: The amount to get from the end of the file.
 
504
 
 
505
        :return: HTTP range header string.
 
506
 
 
507
        At least a non-empty ranges *or* a tail_amount must be
 
508
        provided.
 
509
        """
 
510
        strings = []
 
511
        for offset in ranges:
 
512
            strings.append('%d-%d' % (offset.start,
 
513
                                      offset.start + offset.length - 1))
 
514
 
 
515
        if tail_amount:
 
516
            strings.append('-%d' % tail_amount)
 
517
 
 
518
        return ','.join(strings)
 
519
 
 
520
 
 
521
# TODO: May be better located in smart/medium.py with the other
 
522
# SmartMedium classes
 
523
class SmartClientHTTPMedium(medium.SmartClientMedium):
 
524
 
 
525
    def __init__(self, http_transport):
 
526
        super(SmartClientHTTPMedium, self).__init__(http_transport.base)
 
527
        # We don't want to create a circular reference between the http
 
528
        # transport and its associated medium. Since the transport will live
 
529
        # longer than the medium, the medium keep only a weak reference to its
 
530
        # transport.
 
531
        self._http_transport_ref = weakref.ref(http_transport)
 
532
 
 
533
    def get_request(self):
 
534
        return SmartClientHTTPMediumRequest(self)
 
535
 
 
536
    def should_probe(self):
 
537
        return True
 
538
 
 
539
    def remote_path_from_transport(self, transport):
 
540
        # Strip the optional 'bzr+' prefix from transport so it will have the
 
541
        # same scheme as self.
 
542
        transport_base = transport.base
 
543
        if transport_base.startswith('bzr+'):
 
544
            transport_base = transport_base[4:]
 
545
        rel_url = urlutils.relative_url(self.base, transport_base)
 
546
        return urllib.unquote(rel_url)
 
547
 
 
548
    def send_http_smart_request(self, bytes):
 
549
        try:
 
550
            # Get back the http_transport hold by the weak reference
 
551
            t = self._http_transport_ref()
 
552
            code, body_filelike = t._post(bytes)
 
553
            if code != 200:
 
554
                raise InvalidHttpResponse(
 
555
                    t._remote_path('.bzr/smart'),
 
556
                    'Expected 200 response code, got %r' % (code,))
 
557
        except errors.InvalidHttpResponse, e:
 
558
            raise errors.SmartProtocolError(str(e))
 
559
        return body_filelike
 
560
 
 
561
 
 
562
# TODO: May be better located in smart/medium.py with the other
 
563
# SmartMediumRequest classes
 
564
class SmartClientHTTPMediumRequest(medium.SmartClientMediumRequest):
 
565
    """A SmartClientMediumRequest that works with an HTTP medium."""
 
566
 
 
567
    def __init__(self, client_medium):
 
568
        medium.SmartClientMediumRequest.__init__(self, client_medium)
 
569
        self._buffer = ''
 
570
 
 
571
    def _accept_bytes(self, bytes):
 
572
        self._buffer += bytes
 
573
 
 
574
    def _finished_writing(self):
 
575
        data = self._medium.send_http_smart_request(self._buffer)
 
576
        self._response_body = data
 
577
 
 
578
    def _read_bytes(self, count):
 
579
        """See SmartClientMediumRequest._read_bytes."""
 
580
        return self._response_body.read(count)
 
581
 
 
582
    def _read_line(self):
 
583
        line, excess = medium._get_line(self._response_body.read)
 
584
        if excess != '':
 
585
            raise AssertionError(
 
586
                '_get_line returned excess bytes, but this mediumrequest '
 
587
                'cannot handle excess. (%r)' % (excess,))
 
588
        return line
 
589
 
 
590
    def _finished_reading(self):
 
591
        """See SmartClientMediumRequest._finished_reading."""
 
592
        pass