Skip to content
This repository was archived by the owner on Jan 13, 2021. It is now read-only.

Commit 1a248b5

Browse files
committed
Merge pull request #92 from Lukasa/http11
[WIP] HTTP/1.1
2 parents d374985 + a81e0a2 commit 1a248b5

38 files changed

+2366
-415
lines changed

.travis.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@ python:
77
- pypy
88

99
env:
10-
- TEST_RELEASE=false
11-
- TEST_RELEASE=true
10+
- TEST_RELEASE=false HYPER_FAST_PARSE=false
11+
- TEST_RELEASE=false HYPER_FAST_PARSE=true
12+
- TEST_RELEASE=true HYPER_FAST_PARSE=false
13+
- TEST_RELEASE=true HYPER_FAST_PARSE=true
1214
- NGHTTP2=true
1315

1416
matrix:
1517
allow_failures:
16-
- env: TEST_RELEASE=true
18+
- env: TEST_RELEASE=true HYPER_FAST_PARSE=true
19+
- env: TEST_RELEASE=true HYPER_FAST_PARSE=false
1720
exclude:
1821
- env: NGHTTP2=true
1922
python: pypy

.travis/install.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,9 @@ if [[ "$NGHTTP2" = true ]]; then
3939
sudo ldconfig
4040
fi
4141

42+
if [[ "$HYPER_FAST_PARSE" = true ]]; then
43+
pip install pycohttpparser~=1.0
44+
fi
45+
4246
pip install .
4347
pip install -r test_requirements.txt

HISTORY.rst

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,31 @@
11
Release History
22
===============
33

4+
Upcoming
5+
--------
6+
7+
*New Features*
8+
9+
- HTTP/1.1 support! See the documentation for more. (`Issue #75`_)
10+
- Implementation of a ``HTTPHeaderMap`` data structure that provides dictionary
11+
style lookups while retaining all the semantic information of HTTP headers.
12+
13+
*Major Changes*
14+
15+
- Various changes in the HTTP/2 APIs:
16+
17+
- The ``getheader``, ``getheaders``, ``gettrailer``, and ``gettrailers``
18+
methods on the response object have been removed, replaced instead with
19+
simple ``.headers`` and ``.trailers`` properties that contain
20+
``HTTPHeaderMap`` structures.
21+
- Headers and trailers are now bytestrings, rather than unicode strings.
22+
- An ``iter_chunked()`` method was added to repsonse objects that allows
23+
iterating over data in units of individual data frames.
24+
- Changed the name of ``getresponse()`` to ``get_response()``, because
25+
``getresponse()`` was a terrible name forced upon me by httplib.
26+
27+
.. _Issue #75: https://github.com/Lukasa/hyper/issues/75
28+
429
0.2.2 (2015-04-03)
530
------------------
631

docs/source/advanced.rst

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,54 @@ may want to keep your connections alive only as long as you know you'll need
1313
them. In HTTP/2 this is generally not something you should do unless you're
1414
very confident you won't need the connection again anytime soon. However, if
1515
you decide you want to avoid keeping the connection open, you can use the
16-
:class:`HTTP20Connection <hyper.HTTP20Connection>` as a context manager::
16+
:class:`HTTP20Connection <hyper.HTTP20Connection>` and
17+
:class:`HTTP11Connection <hyper.HTTP11Connection>` as context managers::
1718

1819
with HTTP20Connection('http2bin.org') as conn:
1920
conn.request('GET', '/get')
2021
data = conn.getresponse().read()
2122

2223
analyse(data)
2324

24-
You may not use any :class:`HTTP20Response <hyper.HTTP20Response>` objects
25-
obtained from a connection after that connection is closed. Interacting with
26-
these objects when a connection has been closed is considered undefined
27-
behaviour.
25+
You may not use any :class:`HTTP20Response <hyper.HTTP20Response>` or
26+
:class:`HTTP11Response <hyper.HTTP11Response>` objects obtained from a
27+
connection after that connection is closed. Interacting with these objects when
28+
a connection has been closed is considered undefined behaviour.
29+
30+
Chunked Responses
31+
-----------------
32+
33+
Plenty of APIs return chunked data, and it's often useful to iterate directly
34+
over the chunked data. ``hyper`` lets you iterate over each data frame of a
35+
HTTP/2 response, and each chunk of a HTTP/1.1 response delivered with
36+
``Transfer-Encoding: chunked``::
37+
38+
for chunk in response.read_chunked():
39+
do_something_with_chunk(chunk)
40+
41+
There are some important caveats with this iteration: mostly, it's not
42+
guaranteed that each chunk will be non-empty. In HTTP/2, it's entirely legal to
43+
send zero-length data frames, and this API will pass those through unchanged.
44+
Additionally, by default this method will decompress a response that has a
45+
compressed ``Content-Encoding``: if you do that, each element of the iterator
46+
will no longer be a single chunk, but will instead be whatever the decompressor
47+
returns for that chunk.
48+
49+
If that's problematic, you can set the ``decode_content`` parameter to
50+
``False`` and, if necessary, handle the decompression yourself::
51+
52+
for compressed_chunk in response.read_chunked(decode_content=False):
53+
decompress(compressed_chunk)
54+
55+
Very easy!
2856

2957
Multithreading
3058
--------------
3159

32-
Currently, ``hyper``'s :class:`HTTP20Connection <hyper.HTTP20Connection>` class
33-
is **not** thread-safe. Thread-safety is planned for ``hyper``'s core objects,
34-
but in this early alpha it is not a high priority.
60+
Currently, ``hyper``'s :class:`HTTP20Connection <hyper.HTTP20Connection>` and
61+
:class:`HTTP11Connection <hyper.HTTP11Connection>` classes are **not**
62+
thread-safe. Thread-safety is planned for ``hyper``'s core objects, but in this
63+
early alpha it is not a high priority.
3564

3665
To use ``hyper`` in a multithreaded context the recommended thing to do is to
3766
place each connection in its own thread. Each thread should then have a request
@@ -130,7 +159,7 @@ In order to receive pushed resources, the
130159
with ``enable_push=True``.
131160

132161
You may retrieve the push promises that the server has sent *so far* by calling
133-
:meth:`getpushes() <hyper.HTTP20Connection.getpushes>`, which returns a
162+
:meth:`get_pushes() <hyper.HTTP20Connection.get_pushes>`, which returns a
134163
generator that yields :class:`HTTP20Push <hyper.HTTP20Push>` objects. Note that
135164
this method is not idempotent; promises returned in one call will not be
136165
returned in subsequent calls. If ``capture_all=False`` is passed (the default),
@@ -143,11 +172,11 @@ the original response, or when also processing the original response in a
143172
separate thread (N.B. do not do this; ``hyper`` is not yet thread-safe)::
144173

145174
conn.request('GET', '/')
146-
response = conn.getheaders()
147-
for push in conn.getpushes(): # all pushes promised before response headers
175+
response = conn.get_response()
176+
for push in conn.get_pushes(): # all pushes promised before response headers
148177
print(push.path)
149178
conn.read()
150-
for push in conn.getpushes(): # all other pushes
179+
for push in conn.get_pushes(): # all other pushes
151180
print(push.path)
152181

153182
To cancel an in-progress pushed stream (for example, if the user already has

docs/source/api.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ Primary HTTP/2 Interface
1919
.. autoclass:: hyper.HTTP20Push
2020
:inherited-members:
2121

22+
.. autoclass:: hyper.HTTP11Connection
23+
:inherited-members:
24+
25+
.. autoclass:: hyper.HTTP11Response
26+
:inherited-members:
27+
2228
Headers
2329
-------
2430

docs/source/index.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ Simple. ``hyper`` is written in 100% pure Python, which means no C extensions.
2626
For recent versions of Python (3.4 and onward, and 2.7.9 and onward) it's
2727
entirely self-contained with no external dependencies.
2828

29-
``hyper`` supports Python 3.4 and Python 2.7.9.
29+
``hyper`` supports Python 3.4 and Python 2.7.9, and can speak HTTP/2 and
30+
HTTP/1.1.
3031

3132
Caveat Emptor!
3233
--------------

docs/source/quickstart.rst

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
Quickstart Guide
44
================
55

6-
First, congratulations on picking ``hyper`` for your HTTP/2 needs. ``hyper``
7-
is the premier (and, as far as we're aware, the only) Python HTTP/2 library.
6+
First, congratulations on picking ``hyper`` for your HTTP needs. ``hyper``
7+
is the premier (and, as far as we're aware, the only) Python HTTP/2 library,
8+
as well as a very servicable HTTP/1.1 library.
9+
810
In this section, we'll walk you through using ``hyper``.
911

1012
Installing hyper
@@ -46,8 +48,8 @@ instructions from the `cryptography`_ project, replacing references to
4648

4749
.. _cryptography: https://cryptography.io/en/latest/installation/#installation
4850

49-
Making Your First Request
50-
-------------------------
51+
Making Your First HTTP/2 Request
52+
--------------------------------
5153

5254
With ``hyper`` installed, you can start making HTTP/2 requests. At this
5355
stage, ``hyper`` can only be used with services that *definitely* support
@@ -61,7 +63,7 @@ Begin by getting the homepage::
6163
>>> c = HTTP20Connection('http2bin.org')
6264
>>> c.request('GET', '/')
6365
1
64-
>>> resp = c.getresponse()
66+
>>> resp = c.get_response()
6567

6668
Used in this way, ``hyper`` behaves exactly like ``http.client``. You can make
6769
sequential requests using the exact same API you're accustomed to. The only
@@ -72,13 +74,12 @@ HTTP/2 *stream identifier*. If you're planning to use ``hyper`` in this very
7274
simple way, you can choose to ignore it, but it's potentially useful. We'll
7375
come back to it.
7476

75-
Once you've got the data, things continue to behave exactly like
76-
``http.client``::
77+
Once you've got the data, things diverge a little bit::
7778

78-
>>> resp.getheader('content-type')
79-
'text/html; charset=utf-8'
80-
>>> resp.getheaders()
81-
[('server', 'h2o/1.0.2-alpha1')...
79+
>>> resp.headers['content-type']
80+
[b'text/html; charset=utf-8']
81+
>>> resp.headers
82+
HTTPHeaderMap([(b'server', b'h2o/1.0.2-alpha1')...
8283
>>> resp.status
8384
200
8485

@@ -111,6 +112,41 @@ For example::
111112

112113
``hyper`` will ensure that each response is matched to the correct request.
113114

115+
Making Your First HTTP/1.1 Request
116+
-----------------------------------
117+
118+
With ``hyper`` installed, you can start making HTTP/2 requests. At this
119+
stage, ``hyper`` can only be used with services that *definitely* support
120+
HTTP/2. Before you begin, ensure that whichever service you're contacting
121+
definitely supports HTTP/2. For the rest of these examples, we'll use
122+
Twitter.
123+
124+
You can also use ``hyper`` to make HTTP/1.1 requests. The code is very similar.
125+
For example, to get the Twitter homepage::
126+
127+
>>> from hyper import HTTP11Connection
128+
>>> c = HTTP11Connection('twitter.com:443')
129+
>>> c.request('GET', '/')
130+
>>> resp = c.get_response()
131+
132+
The key difference between HTTP/1.1 and HTTP/2 is that when you make HTTP/1.1
133+
requests you do not get a stream ID. This is, of course, because HTTP/1.1 does
134+
not have streams.
135+
136+
Things behave exactly like they do in the HTTP/2 case, right down to the data
137+
reading::
138+
139+
>>> resp.headers['content-encoding']
140+
[b'deflate']
141+
>>> resp.headers
142+
HTTPHeaderMap([(b'x-xss-protection', b'1; mode=block')...
143+
>>> resp.status
144+
200
145+
>>> body = resp.read()
146+
b'<!DOCTYPE html>\n<!--[if IE 8]><html clas ....
147+
148+
That's all it takes.
149+
114150
Requests Integration
115151
--------------------
116152

hyper/__init__.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,21 @@
1010

1111
from .http20.connection import HTTP20Connection
1212
from .http20.response import HTTP20Response, HTTP20Push
13+
from .http11.connection import HTTP11Connection
14+
from .http11.response import HTTP11Response
1315

1416
# Throw import errors on Python <2.7 and 3.0-3.2.
1517
import sys as _sys
1618
if _sys.version_info < (2,7) or (3,0) <= _sys.version_info < (3,3):
1719
raise ImportError("hyper only supports Python 2.7 and Python 3.3 or higher.")
1820

19-
__all__ = [HTTP20Response, HTTP20Push, HTTP20Connection]
21+
__all__ = [
22+
HTTP20Response,
23+
HTTP20Push,
24+
HTTP20Connection,
25+
HTTP11Connection,
26+
HTTP11Response,
27+
]
2028

2129
# Set default logging handler.
2230
import logging

hyper/cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ def get_content_type_and_charset(response):
216216
def request(args):
217217
conn = HTTP20Connection(args.url.host, args.url.port)
218218
conn.request(args.method, args.url.path, args.body, args.headers)
219-
response = conn.getresponse()
219+
response = conn.get_response()
220220
log.debug('Response Headers:\n%s', pformat(response.getheaders()))
221221
ctype, charset = get_content_type_and_charset(response)
222222
data = response.read().decode(charset)

hyper/http20/bufsocket.py renamed to hyper/common/bufsocket.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,20 @@ def can_read(self):
7676

7777
return False
7878

79+
@property
80+
def buffer(self):
81+
"""
82+
Get access to the buffer itself.
83+
"""
84+
return self._buffer_view[self._index:self._buffer_end]
85+
86+
def advance_buffer(self, count):
87+
"""
88+
Advances the buffer by the amount of data consumed outside the socket.
89+
"""
90+
self._index += count
91+
self._bytes_in_buffer -= count
92+
7993
def new_buffer(self):
8094
"""
8195
This method moves all the data in the backing buffer to the start of
@@ -145,6 +159,23 @@ def recv(self, amt):
145159

146160
return data
147161

162+
def fill(self):
163+
"""
164+
Attempts to fill the buffer as much as possible. It will block for at
165+
most the time required to have *one* ``recv_into`` call return.
166+
"""
167+
if not self._remaining_capacity:
168+
self.new_buffer()
169+
170+
count = self._sck.recv_into(self._buffer_view[self._buffer_end:])
171+
if not count:
172+
raise ConnectionResetError()
173+
174+
self._bytes_in_buffer += count
175+
176+
return
177+
178+
148179
def readline(self):
149180
"""
150181
Read up to a newline from the network and returns it. The implicit

0 commit comments

Comments
 (0)