Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 17 additions & 24 deletions src/hip/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from ._request import Method, Request
from ._response import Response
from ._urls import URL
from .transports import ConnectionPool, ResponseStream, Transport
from .transports import ConnectionPool, SendContext, Transport

__all__ = ["Client"]

Expand Down Expand Up @@ -57,26 +57,16 @@ def request(
content: Content | bytes | None = None,
) -> Response:
request = self.build_request(method, url, headers=headers, content=content)
with self.via.send(request) as stream:
ct = stream.headers.get("Content-Type", "application/octet-stream")
with self.via.send(request) as (status_code, resp_headers, stream):
ct = resp_headers.get("Content-Type", "application/octet-stream")
cls = select_media(self.media, ct)
content = cls.parse(stream)
return Response(
status_code=stream.status_code,
headers=stream.headers,
status_code=status_code,
headers=resp_headers,
content=content,
)

def stream(
self,
method: Method | str,
url: URL | str,
headers: Headers | dict[str, str] | None = None,
content: Content | bytes | None = None,
) -> ResponseStream:
request = self.build_request(method, url, headers=headers, content=content)
return self.via.send(request)

def get(
self,
url: URL | str,
Expand Down Expand Up @@ -138,15 +128,17 @@ def __init__(self, transport: Transport) -> None:
self._transport = transport

def build_redirect_request(
self, request: Request, response: ResponseStream
self, request: Request, ctx: SendContext
) -> Request | None:
status, headers, _ = ctx

# Redirect status codes...
if response.status_code not in (301, 302, 303, 307, 308):
if status not in (301, 302, 303, 307, 308):
return None

# Redirects need a valid location header...
try:
location = URL(response.headers["Location"])
location = URL(headers["Location"])
except (KeyError, ValueError):
return None

Expand All @@ -158,22 +150,23 @@ def build_redirect_request(

return Request(method, url, headers, content)

def send(self, request: Request) -> ResponseStream:
def send(self, request: Request) -> SendContext:
while True:
response = self._transport.send(request)
ctx = self._transport.send(request)

# Determine if we have a redirect or not.
redirect = self.build_redirect_request(request, response)
redirect = self.build_redirect_request(request, ctx)

# If we don't have a redirect, we're done.
if redirect is None:
return response
return ctx

# If we have a redirect, then we read the body of the response.
# Ensures that the HTTP connection is available for a new
# request/response cycle.
with response:
response.read()
with ctx:
_, _, stream = ctx
stream.read()

# Make the next request
request = redirect
Expand Down
4 changes: 2 additions & 2 deletions src/hip/transports/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from .base import ResponseStream, Transport
from .base import SendContext, Transport
from .connection import Connection, open_connection
from .pool import ConnectionPool

__all__ = [
"Connection",
"ConnectionPool",
"ResponseStream",
"SendContext",
"Transport",
"open_connection",
]
36 changes: 7 additions & 29 deletions src/hip/transports/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,10 @@
from .._streams import Stream
from .._urls import URL

__all__ = ["ResponseStream", "Transport"]
__all__ = ["SendContext", "Transport"]


class ResponseStream(Stream):
def __init__(
self, status_code: StatusCode, headers: Headers, stream: Stream
) -> None:
self.status_code = status_code
self.headers = headers
self.stream = stream

def read(self, size: int = -1) -> bytes:
return self.stream.read(size)

def close(self):
self.stream.close()

class SendContext(tuple[StatusCode, Headers, Stream]):
def __enter__(self):
return self

Expand All @@ -33,11 +20,12 @@ def __exit__(
exc_value: BaseException | None = None,
traceback: types.TracebackType | None = None,
) -> None:
self.close()
_, _, stream = self
stream.close()


class Transport:
def send(self, request: Request) -> ResponseStream:
def send(self, request: Request) -> SendContext:
raise NotImplementedError()

def close(self):
Expand All @@ -51,16 +39,6 @@ def request(
content: Content | bytes | None = None,
) -> Response:
request = Request(method, url, headers=headers, content=content)
with self.send(request) as stream:
with self.send(request) as (status, resp_headers, stream):
body = stream.read()
return Response(stream.status_code, headers=stream.headers, content=body)

def stream(
self,
method: Method | str,
url: URL | str,
headers: Headers | dict[str, str] | None = None,
content: Content | bytes | None = None,
) -> ResponseStream:
request = Request(method, url, headers=headers, content=content)
return self.send(request)
return Response(status, headers=resp_headers, content=body)
21 changes: 5 additions & 16 deletions src/hip/transports/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from .._streams import Stream
from .._urls import URL
from ..backends.sync import Lock, NetworkBackend
from .base import ResponseStream, Transport
from .base import SendContext, Transport

__all__ = ["Connection", "open_connection"]

Expand Down Expand Up @@ -43,14 +43,14 @@ def description(self) -> str:
return self._parser.description()

# API entry points...
def send(self, request: Request) -> ResponseStream:
def send(self, request: Request) -> SendContext:
# async with self._request_lock:
# try:
self._send_head(request)
self._send_body(request)
code, headers = self._recv_head()
stream = self._recv_body(headers)
return ResponseStream(code, headers, stream)
return SendContext((code, headers, stream))

def close(self) -> None:
with self._request_lock:
Expand All @@ -66,20 +66,9 @@ def request(
) -> Response:
url = self._origin.join(url)
request = Request(method, url, headers=headers, content=content)
with self.send(request) as stream:
with self.send(request) as (status_code, resp_headers, stream):
body = stream.read()
return Response(stream.status_code, headers=stream.headers, content=body)

def stream(
self,
method: Method | str,
url: URL | str,
headers: Headers | dict[str, str] | None = None,
content: Content | bytes | None = None,
) -> ResponseStream:
url = self._origin.join(url)
request = Request(method, url, headers=headers, content=content)
return self.send(request)
return Response(status_code, headers=resp_headers, content=body)

# Send the request...
def _send_head(self, request: Request) -> None:
Expand Down
4 changes: 2 additions & 2 deletions src/hip/transports/pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from .._request import Request
from .._urls import URL
from ..backends.sync import NetworkBackend, Semaphore
from .base import ResponseStream, Transport
from .base import SendContext, Transport
from .connection import (
Connection,
open_connection,
Expand All @@ -31,7 +31,7 @@ def __init__(self, backend: NetworkBackend | None = None):
self._closed = False

# Public API...
def send(self, request: Request) -> ResponseStream:
def send(self, request: Request) -> SendContext:
if self._closed:
raise RuntimeError("ConnectionPool is closed.")

Expand Down
45 changes: 24 additions & 21 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,10 @@ def test_request(client, server):


def test_stream(client, server):
with client.stream("GET", server.url) as r:
assert r.status_code == 200
body = r.read()
request = hip.Request("GET", server.url)
with client.transport.send(request) as (status, _, stream):
assert status == 200
body = stream.read()
Comment on lines +153 to +155

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very neat 👌🏻

assert json.loads(body) == {
"method": "GET",
"query-params": {},
Expand All @@ -161,10 +162,12 @@ def test_stream(client, server):


def test_stream_read_after_eof(client, server):
with client.stream("GET", server.url) as response:
assert response.read()
assert response.read() == b""
assert response.read() == b""
request = hip.Request("GET", server.url)
with client.transport.send(request) as (status, _, stream):
assert status == 200
assert stream.read()
assert stream.read() == b""
assert stream.read() == b""


def test_keepalive(client, server):
Expand Down Expand Up @@ -269,15 +272,15 @@ def app(request):


def test_client_streams_gzip_response(client):
with (
hip.Server(gzip_response) as server,
client.stream("GET", server.url) as response,
):
assert response.read(5) == b"Hello"
assert response.read(5) == b", wor"
assert response.read(5) == b"ld."
assert response.read(5) == b""
assert response.read(5) == b""
with hip.Server(gzip_response) as server:
request = hip.Request("GET", server.url, {"Accept-Encoding": "gzip"})
with client.transport.send(request) as (status, _, stream):
assert status == 200
assert stream.read(5) == b"Hello"
assert stream.read(5) == b", wor"
assert stream.read(5) == b"ld."
assert stream.read(5) == b""
assert stream.read(5) == b""


def test_client_rejects_truncated_gzip_response(client):
Expand Down Expand Up @@ -332,8 +335,8 @@ def app(request):
content=gzipped,
)

with (
hip.Server(app) as server,
client.stream("GET", server.url) as response,
):
assert response.read() == source
with hip.Server(app) as server:
request = hip.Request("GET", server.url, {"Accept-Encoding": "gzip"})
with client.transport.send(request) as (status, _, stream):
assert status == 200
assert stream.read() == source
28 changes: 15 additions & 13 deletions tests/test_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,12 @@ def test_connection_pool_stream(server):
assert repr(pool) == "<ConnectionPool [0 active]>"
assert len(pool.connections) == 0

with pool.stream("GET", server.url) as r:
assert r.status_code == 200
request = hip.Request("GET", server.url)
with pool.send(request) as (status, _, stream):
assert status == 200
assert repr(pool) == "<ConnectionPool [1 active]>"
assert len(pool.connections) == 1
r.read()
stream.read()

assert repr(pool) == "<ConnectionPool [0 active, 1 idle]>"
assert len(pool.connections) == 1
Expand Down Expand Up @@ -92,21 +93,22 @@ def test_connection_request(server):
def test_connection_stream(server):
with hip.transports.open_connection(server.url) as conn:
assert repr(conn) == f"<Connection [{server.url} idle]>"
with conn.stream("GET", "/") as r:
assert r.status_code == 200
request = hip.Request("GET", server.url)
with conn.send(request) as (status, _, stream):
assert status == 200
assert repr(conn) == f"<Connection [{server.url} active]>"
r.read()
stream.read()
assert repr(conn) == f"<Connection [{server.url} idle]>"


def test_connection_stream_read_in_chunks(server):
with (
hip.transports.open_connection(server.url) as conn,
conn.stream("GET", "/") as response,
):
assert response.read(5) == b"Hello"
assert response.read(5) == b", wor"
assert response.read() == b"ld."
with hip.transports.open_connection(server.url) as conn:
request = hip.Request("GET", server.url)
with conn.send(request) as (status, _, stream):
assert status == 200
assert stream.read(5) == b"Hello"
assert stream.read(5) == b", wor"
assert stream.read() == b"ld."


class TLSBackend:
Expand Down
Loading