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
15 changes: 11 additions & 4 deletions docs/clients.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ Client instances can be configured with a base URL that is used when constructin
```python
>>> cli = httpx.Client(url="https://www.httpbin.org")
>>> r = cli.get("/json")
>>> r.url
>>> r
<Response [200 OK]>
>>> r.request.url
'https://www.httpbin.org/json'
```

Expand All @@ -56,15 +58,20 @@ You can override this behavior by explicitly specifying the default headers...
```python
>>> headers = {"User-Agent": "dev", "Accept-Encoding": "gzip"}
>>> cli = httpx.Client(headers=headers)
>>> r = cli.get("")
>>> r = cli.get("https://www.example.com/")
```

## Configuring the connection pool

The connection pool used by the client can be configured in order to customise the SSL context, the maximum number of concurrent connections, or the network backend.

```python
>>> transport = httpx.ConnectionPool(ssl_context=httpx.SSLNoVerify())
>>> # Setup an SSL context to allow connecting to improperly configured SSL.
>>> no_verify = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
>>> no_verify.check_hostname = False
>>> no_verify.verify_mode = ssl.CERT_NONE
>>> # Instantiate a client with our custom SSL context.
>>> transport = httpx.ConnectionPool(ssl_context=no_verify)
>>> cli = httpx.Client(transport=transport)
```

Expand Down Expand Up @@ -101,7 +108,7 @@ class MockTransport(httpx.Transport):
self._response = response

@contextlib.contextmanager
def send(request):
def send(self, request):
yield response

def close(self):
Expand Down
19 changes: 14 additions & 5 deletions src/ahttpx/__init__.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,37 @@
from ._client import * # Client, open_client
from ._models import * # Content, File, Files, Form, Headers, JSON, MultiPart, Response, Request
from ._models import * # Content, File, Files, Form, Headers, JSON, MultiPart, Response, Request, Text
from ._network import * # NetworkBackend, NetworkStream
from ._pool import * # Connection, HTTPTransport, Transport
from ._urls import * # InvalidURL, QueryParams, URL
from ._pool import * # Connection, ConnectionPool, Transport
from ._urls import * # QueryParams, URL


__all__ = [
"Client",
"Connection",
"ConnectionPool",
"Content",
"File",
"Files",
"Form",
"Headers",
"HTTPTransport",
"InvalidURL",
"JSON",
"MultiPart",
"NetworkBackend",
"NetworkStream",
"open_client",
"Response",
"Request",
"Text",
"Transport",
"QueryParams",
"URL",
]


# Modules names are deliberately private here.
# We fix-up the public API space so that class `__repr__` properly reflects this...
#
# >>> httpx.Client
# <class 'httpx.Client'>
for attr in __all__:
setattr(locals()[attr], '__module__', 'httpx')
15 changes: 9 additions & 6 deletions src/ahttpx/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,29 @@
from typing import AsyncIterable, AsyncIterator, Mapping

from ._models import Content, Headers, Response, Request
from ._pool import ConnectionPool
from ._pool import ConnectionPool, Transport
from ._urls import URL

__all__ = ["Client", "open_client"]
__all__ = ["Client", "Content", "open_client"]


class Client:
def __init__(
self,
url: URL | str | None = None,
headers: Headers | Mapping[str, str] | None = None
headers: Headers | Mapping[str, str] | None = None,
transport: Transport | None = None,
):
if url is None:
url = ""
if headers is None:
headers = {"User-Agent": "dev"}
if transport is None:
transport = ConnectionPool()

self.url = URL(url)
self.headers = Headers(headers)
self.transport = ConnectionPool()
self.transport = transport
self.via = RedirectMiddleware(self.transport)

def build_request(
Expand Down Expand Up @@ -109,8 +112,8 @@ def __repr__(self):
return f"<Client [{self.transport.description()}]>"


class RedirectMiddleware:
def __init__(self, transport) -> None:
class RedirectMiddleware(Transport):
def __init__(self, transport: Transport) -> None:
self._transport = transport

def is_redirect(self, response: Response) -> bool:
Expand Down
11 changes: 11 additions & 0 deletions src/ahttpx/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"MultiPart",
"Response",
"Request",
"Text",
]

# We're using the same set as stdlib `http.HTTPStatus` here...
Expand Down Expand Up @@ -747,6 +748,16 @@ def encode(self) -> tuple[Headers, bytes | AsyncIterable[bytes]]:
return (headers, content)


class Text(Content):
def __init__(self, text: str) -> None:
self._text = text

def encode(self) -> tuple[Headers, bytes | AsyncIterable[bytes]]:
content = self._text.encode("utf-8")
headers = Headers({"Content-Type": "text/plain; charset='utf-8", "Content-Length": str(len(content))})
return (headers, content)


class MultiPart(Content):
def __init__(
self,
Expand Down
16 changes: 14 additions & 2 deletions src/ahttpx/_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,16 @@
from ._network import Lock, NetworkBackend, Semaphore, NetworkStream


class ConnectionPool:
class Transport:
@contextlib.asynccontextmanager
async def send(self, request: Request) -> typing.AsyncIterator[Response]:
raise NotImplementedError()

async def close(self):
pass


class ConnectionPool(Transport):
def __init__(self):
self._connections = []
self._ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
Expand All @@ -21,6 +30,9 @@ def __init__(self):
# Public API...
@contextlib.asynccontextmanager
async def send(self, request: Request) -> typing.AsyncIterator[Response]:
if request.url.scheme not in ("http", "https"):
raise ValueError(f"Invalid URL {str(request.url)!r}. Scheme must be http or https.")

async with self._limit_concurrency:
try:
connection = await self._get_connection(request)
Expand Down Expand Up @@ -91,7 +103,7 @@ def __exit__(
self.close()


class Connection:
class Connection(Transport):
def __init__(self, stream: "NetworkStream", origin: URL | str):
self._stream = stream
self._origin = URL(origin)
Expand Down
19 changes: 14 additions & 5 deletions src/httpx/__init__.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,37 @@
from ._client import * # Client, open_client
from ._models import * # Content, File, Files, Form, Headers, JSON, MultiPart, Response, Request
from ._models import * # Content, File, Files, Form, Headers, JSON, MultiPart, Response, Request, Text
from ._network import * # NetworkBackend, NetworkStream
from ._pool import * # Connection, HTTPTransport, Transport
from ._urls import * # InvalidURL, QueryParams, URL
from ._pool import * # Connection, ConnectionPool, Transport
from ._urls import * # QueryParams, URL


__all__ = [
"Client",
"Connection",
"ConnectionPool",
"Content",
"File",
"Files",
"Form",
"Headers",
"HTTPTransport",
"InvalidURL",
"JSON",
"MultiPart",
"NetworkBackend",
"NetworkStream",
"open_client",
"Response",
"Request",
"Text",
"Transport",
"QueryParams",
"URL",
]


# Modules names are deliberately private here.
# We fix-up the public API space so that class `__repr__` properly reflects this...
#
# >>> httpx.Client
# <class 'httpx.Client'>
for attr in __all__:
setattr(locals()[attr], '__module__', 'httpx')
11 changes: 7 additions & 4 deletions src/httpx/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,29 @@
from typing import Iterable, Iterator, Mapping

from ._models import Content, Headers, Response, Request
from ._pool import ConnectionPool
from ._pool import ConnectionPool, Transport
from ._urls import URL

__all__ = ["Client", "open_client"]
__all__ = ["Client", "Content", "open_client"]


class Client:
def __init__(
self,
url: URL | str | None = None,
headers: Headers | Mapping[str, str] | None = None
headers: Headers | Mapping[str, str] | None = None,
transport: Transport | None = None,
):
if url is None:
url = ""
if headers is None:
headers = {"User-Agent": "dev"}
if transport is None:
transport = ConnectionPool()

self.url = URL(url)
self.headers = Headers(headers)
self.transport = ConnectionPool()
self.transport = transport
self.via = RedirectMiddleware(self.transport)

def build_request(
Expand Down
11 changes: 11 additions & 0 deletions src/httpx/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"MultiPart",
"Response",
"Request",
"Text",
]

# We're using the same set as stdlib `http.HTTPStatus` here...
Expand Down Expand Up @@ -747,6 +748,16 @@ def encode(self) -> tuple[Headers, bytes | Iterable[bytes]]:
return (headers, content)


class Text(Content):
def __init__(self, text: str) -> None:
self._text = text

def encode(self) -> tuple[Headers, bytes | Iterable[bytes]]:
content = self._text.encode("utf-8")
headers = Headers({"Content-Type": "text/plain; charset='utf-8", "Content-Length": str(len(content))})
return (headers, content)


class MultiPart(Content):
def __init__(
self,
Expand Down
16 changes: 14 additions & 2 deletions src/httpx/_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,16 @@
from ._network import Lock, NetworkBackend, Semaphore, NetworkStream


class ConnectionPool:
class Transport:
@contextlib.contextmanager
def send(self, request: Request) -> typing.Iterator[Response]:
raise NotImplementedError()

def close(self):
pass


class ConnectionPool(Transport):
def __init__(self):
self._connections = []
self._ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
Expand All @@ -21,6 +30,9 @@ def __init__(self):
# Public API...
@contextlib.contextmanager
def send(self, request: Request) -> typing.Iterator[Response]:
if request.url.scheme not in ("http", "https"):
raise ValueError(f"Invalid URL {str(request.url)!r}. Scheme must be http or https.")

with self._limit_concurrency:
try:
connection = self._get_connection(request)
Expand Down Expand Up @@ -91,7 +103,7 @@ def __exit__(
self.close()


class Connection:
class Connection(Transport):
def __init__(self, stream: "NetworkStream", origin: URL | str):
self._stream = stream
self._origin = URL(origin)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_00_quickstart.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

def test_cli():
cli = httpx.Client()
assert repr(cli) == "<Client>"
assert repr(cli) == "<Client [0 active]>"


# def test_post(httpbin):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_01_clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def cli():

def test_client():
client = httpx.Client()
assert repr(client) == "<Client>"
assert repr(client) == "<Client [0 active]>"


def test_get(httpbin, cli):
Expand Down