</a>
</p>
-**Note**: *This project should be considered as an "alpha" release. It is substantially API complete, but there are still some areas that need more work.*
+HTTPX is an asynchronous HTTP client, that supports HTTP/2 and HTTP/1.1.
+
+It can be used in high-performance async web frameworks, using either asyncio
+or trio, and is able to support making large numbers of requests concurrently.
+
+**Note**: *The 0.8 release switched HTTPX into focusing exclusively on the async
+client. It is possible that we'll look at re-introducing a sync API at a
+later date.*
---
Let's get started...
+*The standard Python REPL does not allow top-level async statements.*
+
+*To run async examples directly you'll probably want to either use `ipython`,
+or use Python 3.8 with `python -m asyncio`.*
+
```python
>>> import httpx
->>> r = httpx.get('https://www.example.org/')
+>>> r = await httpx.get('https://www.example.org/')
>>> r
<Response [200 OK]>
>>> r.status_code
HTTPX builds on the well-established usability of `requests`, and gives you:
-* A requests-compatible API.
+* A requests-compatible API wherever possible.
* HTTP/2 and HTTP/1.1 support.
-* Support for [issuing HTTP requests in parallel](https://www.encode.io/httpx/parallel/). *(Coming soon)*
-* Standard synchronous interface, but [with `async`/`await` support if you need it](https://www.encode.io/httpx/async/).
-* Ability to [make requests directly to WSGI or ASGI applications](https://www.encode.io/httpx/advanced/#calling-into-python-web-apps).
+* Ability to [make requests directly to ASGI applications](https://www.encode.io/httpx/advanced/#calling-into-python-web-apps).
* Strict timeouts everywhere.
* Fully type annotated.
* 100% test coverage.
* Automatic Content Decoding
* Unicode Response Bodies
* Multipart File Uploads
-* HTTP(S) Proxy Support *(TODO)*
+* HTTP(S) Proxy Support
* Connection Timeouts
* Streaming Downloads
* .netrc Support
For a run-through of all the basics, head over to the [QuickStart](https://www.encode.io/httpx/quickstart/).
-For more advanced topics, see the [Advanced Usage](https://www.encode.io/httpx/advanced/) section, or
-the specific topics on making [Parallel Requests](https://www.encode.io/httpx/parallel/) or using the
-[Async Client](https://www.encode.io/httpx/async/).
+For more advanced topics, see the [Advanced Usage](https://www.encode.io/httpx/advanced/) section.
The [Developer Interface](https://www.encode.io/httpx/api/) provides a comprehensive API reference.
The recommended way to use a `Client` is as a context manager. This will ensure that connections are properly cleaned up when leaving the `with` block:
```python
->>> with httpx.Client() as client:
-... r = client.get('https://example.com')
-...
+>>> async with httpx.Client() as client:
+... r = await client.get('https://example.com')
+...
>>> r
<Response [200 OK]>
```
```python
>>> client = httpx.Client()
>>> try:
-... r = client.get('https://example.com')
+... r = await client.get('https://example.com')
... finally:
-... client.close()
-...
+... await client.close()
+...
>>> r
<Response [200 OK]>
```
```python
>>> url = 'http://httpbin.org/headers'
>>> headers = {'user-agent': 'my-app/0.0.1'}
->>> with httpx.Client(headers=headers) as client:
-... r = client.get(url)
+>>> async with httpx.Client(headers=headers) as client:
+... r = await client.get(url)
...
>>> r.json()['headers']['User-Agent']
'my-app/0.0.1'
!!! note
When you provide a parameter at both the client and request levels, one of two things can happen:
-
+
- For headers, query parameters and cookies, the values are merged into one.
- For all other parameters, the request-level value is used.
One particularly useful parameter is `base_url`, which allows you to define a base URL to prepend to all outgoing requests:
```python
->>> with httpx.Client(base_url='http://httpbin.org') as client:
-... r = client.get('/headers')
-...
+>>> async with httpx.Client(base_url='http://httpbin.org') as client:
+... r = await client.get('/headers')
+...
>>> r.request.url
URL('http://httpbin.org/headers')
```
## Calling into Python Web Apps
You can configure an `httpx` client to call directly into a Python web
-application using either the WSGI or ASGI protocol.
+application using either the ASGI protocol.
This is particularly useful for two main use-cases:
* Using `httpx` as a client inside test cases.
* Mocking out external services during tests or in dev/staging environments.
-Here's an example of integrating against a Flask application:
+Let's take this Starlette application as an example:
```python
-from flask import Flask
-import httpx
+from starlette.applications import Starlette
+from starlette.responses import HTMLResponse
+from starlette.routing import Route
+
+async def hello():
+ return HTMLResponse("Hello World!")
-app = Flask(__name__)
-@app.route("/")
-def hello():
- return "Hello World!"
+app = Starlette(routes=[Route("/", hello)])
+```
+
+We can make requests directly against the application, like so:
-with httpx.Client(app=app) as client:
- r = client.get('http://example/')
- assert r.status_code == 200
- assert r.text == "Hello World!"
+```python
+>>> import httpx
+>>> async with httpx.Client(app=app) as client:
+... r = client.get('http://example/')
+... assert r.status_code == 200
+... assert r.text == "Hello World!"
```
For some more complex cases you might need to customize the WSGI or ASGI
dispatch. This allows you to:
* Inspect 500 error responses rather than raise exceptions by setting `raise_app_exceptions=False`.
-* Mount the WSGI or ASGI application at a subpath by setting `script_name` (WSGI) or `root_path` (ASGI).
-* Use a given client address for requests by setting `remote_addr` (WSGI) or `client` (ASGI).
+* Mount the WSGI or ASGI application at a subpath by setting `root_path`.
+* Use a given client address for requests by setting `client`.
For example:
```python
# Instantiate a client that makes WSGI requests with a client IP of "1.2.3.4".
-dispatch = httpx.dispatch.WSGIDispatch(app=app, remote_addr="1.2.3.4")
-with httpx.Client(dispatch=dispatch) as client:
+dispatch = httpx.dispatch.ASGIDispatch(app=app, remote_addr="1.2.3.4")
+async with httpx.Client(dispatch=dispatch) as client:
...
```
make modifications before sending the request.
```python
->>> with httpx.Client() as client:
+>>> async with httpx.Client() as client:
... req = client.build_request("OPTIONS", "https://example.com")
... req.url.full_path = "*" # Build an 'OPTIONS *' request for CORS
-... r = client.send(req)
+... r = await client.send(req)
...
>>> r
<Response [200 OK]>
For example:
```python
-with httpx.Client(http_versions=["HTTP/1.1"]) as h11_client:
- h11_response = h11_client.get("https://myserver.com")
+async with httpx.Client(http_versions=["HTTP/1.1"]) as h11_client:
+ h11_response = await h11_client.get("https://myserver.com")
-with httpx.Client(http_versions=["HTTP/2"]) as h2_client:
- h2_response = h2_client.get("https://myserver.com")
+async with httpx.Client(http_versions=["HTTP/2"]) as h2_client:
+ h2_response = await h2_client.get("https://myserver.com")
```
## .netrc Support
As default `trust_env` is true. To set false:
```python
->>> httpx.get('https://example.org/', trust_env=False)
+>>> await httpx.get('https://example.org/', trust_env=False)
```
If `NETRC` environment is empty, HTTPX tries to use default files.
import httpx
-with httpx.Client(uds="/var/run/docker.sock") as client:
+async with httpx.Client(uds="/var/run/docker.sock") as client:
# This request will connect through the socket file.
- resp = client.get("http://localhost/version")
+ resp = await client.get("http://localhost/version")
```
## HTTP Proxying
... "http": "http://127.0.0.1:3080",
... "https": "http://127.0.0.1:3081"
... }
->>> with httpx.Client(proxies=proxies) as client:
+>>> async with httpx.Client(proxies=proxies) as client:
... ...
```
... "http": "...", # Scheme
... "all": "...", # All
... }
->>> with httpx.Client(proxies=proxies) as client:
+>>> async with httpx.Client(proxies=proxies) as client:
... ...
-...
+...
>>> proxy = "..." # Shortcut for {'all': '...'}
->>> with httpx.Client(proxies=proxy) as client:
+>>> async with httpx.Client(proxies=proxy) as client:
... ...
```
proxy_url="https://127.0.0.1",
proxy_mode=httpx.HTTPProxyMode.TUNNEL_ONLY
)
-with httpx.Client(proxies=proxy) as client:
+async with httpx.Client(proxies=proxy) as client:
# This request will be tunneled instead of forwarded.
- r = client.get("http://example.com")
+ r = await client.get("http://example.com")
```
!!! note
```python
# Using top-level API
-httpx.get('http://example.com/api/v1/example', timeout=5)
+await httpx.get('http://example.com/api/v1/example', timeout=5)
# Or, with a client:
-with httpx.Client() as client:
- client.get("http://example.com/api/v1/example", timeout=5)
+async with httpx.Client() as client:
+ await client.get("http://example.com/api/v1/example", timeout=5)
```
- On a client instance, which results in the given `timeout` being used as a default for requests made with this client:
```python
-with httpx.Client(timeout=5) as client:
- client.get('http://example.com/api/v1/example')
+async with httpx.Client(timeout=5) as client:
+ await client.get('http://example.com/api/v1/example')
```
Besides, you can pass timeouts in two forms:
write_timeout=15
)
-resp = httpx.get('http://example.com/api/v1/example', timeout=timeout)
+resp = await httpx.get('http://example.com/api/v1/example', timeout=timeout)
```
### Default timeouts
```python
url = "http://example.com/api/v1/delay/10"
-httpx.get(url, timeout=None) # Times out after 5s
+await httpx.get(url, timeout=None) # Times out after 5s
-with httpx.Client(timeout=None) as client:
- client.get(url) # Does not timeout, returns after 10s
+async with httpx.Client(timeout=None) as client:
+ await client.get(url) # Does not timeout, returns after 10s
timeout = httpx.TimeoutConfig(
read_timeout=None,
write_timeout=5
)
-httpx.get(url, timeout=timeout) # Does not timeout, returns after 10s
+await httpx.get(url, timeout=timeout) # Does not timeout, returns after 10s
```
## Multipart file encoding
```python
>>> files = {'upload-file': ('report.xls', open('report.xls', 'rb'), 'application/vnd.ms-excel')}
->>> r = httpx.post("https://httpbin.org/post", files=files)
+>>> r = await httpx.post("https://httpbin.org/post", files=files)
>>> print(r.text)
{
...
```python
>>> files = {'upload-file': (None, 'text content', 'text/plain')}
->>> r = httpx.post("https://httpbin.org/post", files=files)
+>>> r = await httpx.post("https://httpbin.org/post", files=files)
>>> print(r.text)
{
...
```python
import httpx
-r = httpx.get("https://example.org", verify="path/to/client.pem")
+r = await httpx.get("https://example.org", verify="path/to/client.pem")
```
### Making HTTPS requests to a local server
```python
>>> import httpx
->>> r = httpx.get("https://localhost:8000", verify="/tmp/client.pem")
+>>> r = await httpx.get("https://localhost:8000", verify="/tmp/client.pem")
>>> r
Response <200 OK>
```
+
+## Support async environments
+
+### [asyncio](https://docs.python.org/3/library/asyncio.html) (Default)
+
+By default, `AsyncClient` uses `asyncio` to perform asynchronous operations and I/O calls.
+
+### [trio](https://github.com/python-trio/trio)
+
+To make asynchronous requests in `trio` programs, pass a `TrioBackend` to the `AsyncClient`:
+
+```python
+import trio
+import httpx
+from httpx.concurrency.trio import TrioBackend
+
+async def main():
+ async with httpx.AsyncClient(backend=TrioBackend()) as client:
+ ...
+
+trio.run(main)
+```
+
+!!! important
+ `trio` must be installed to import and use the `TrioBackend`.
::: httpx.Client
:docstring:
- :members: headers cookies params request get head options post put patch delete build_request send close
+ :members: headers cookies params request get head options post put patch delete build_request send close
## `Response`
```python
>>> request = httpx.Request("GET", "https://example.org", headers={'host': 'example.org'})
->>> response = client.send(request)
+>>> response = await client.send(request)
```
* `def __init__(method, url, [params], [data], [json], [headers], [cookies])`
+++ /dev/null
-# Async Client
-
-HTTPX offers a standard synchronous API by default, but also gives you
-the option of an async client if you need it.
-
-Async is a concurrency model that is far more efficient than multi-threading,
-and can provide significant performance benefits and enable the use of
-long-lived network connections such as WebSockets.
-
-If you're working with an async web framework then you'll also want to use an
-async client for sending outgoing HTTP requests.
-
-## Making Async requests
-
-To make asynchronous requests, you'll need an `AsyncClient`.
-
-```python
->>> async with httpx.AsyncClient() as client:
->>> r = await client.get('https://www.example.com/')
->>> r
-<Response [200 OK]>
-```
-
-!!! tip
- Use [IPython](https://ipython.readthedocs.io/en/stable/) to try this code interactively, as it supports executing `async`/`await` expressions in the console.
-
-!!! note
- The `async with` syntax ensures that all active connections are closed on exit.
-
- It is safe to access response content (e.g. `r.text`) both inside and outside the `async with` block, unless you are using response streaming. In that case, you should `.read()`, `.stream()`, or `.close()` the response *inside* the `async with` block.
-
-## API Differences
-
-If you're using streaming responses then there are a few bits of API that
-use async methods:
-
-```python
->>> async with httpx.AsyncClient() as client:
->>> r = await client.get('https://www.example.com/', stream=True)
->>> async for chunk in r.stream():
->>> ...
-```
-
-The async response methods are:
-
-* `.read()`
-* `.stream()`
-* `.raw()`
-* `.close()`
-
-If you're making [parallel requests](parallel.md), then you'll also need to use an async API:
-
-```python
->>> async with httpx.AsyncClient() as client:
->>> async with client.parallel() as parallel:
->>> pending_one = parallel.get('https://example.com/1')
->>> pending_two = parallel.get('https://example.com/2')
->>> response_one = await pending_one.get_response()
->>> response_two = await pending_two.get_response()
-```
-
-The async parallel methods are:
-
-* `.parallel()` *Used as an "async with" context manager.*
-* `.get_response()`
-* `.next_response()`
-
-## Supported async libraries
-
-You can use `AsyncClient` with any of the following async libraries.
-
-!!! tip
- You will typically be using `AsyncClient` in async programs that run on `asyncio`. If that's the case, or if you're not sure what this is all about, you can safely ignore this section.
-
-### [asyncio](https://docs.python.org/3/library/asyncio.html) (Default)
-
-By default, `AsyncClient` uses `asyncio` to perform asynchronous operations and I/O calls.
-
-```python
-import asyncio
-import httpx
-
-async def main():
- async with httpx.AsyncClient() as client:
- ...
-
-asyncio.run(main())
-```
-
-### [trio](https://github.com/python-trio/trio)
-
-To make asynchronous requests in `trio` programs, pass a `TrioBackend` to the `AsyncClient`:
-
-```python
-import trio
-import httpx
-from httpx.concurrency.trio import TrioBackend
-
-async def main():
- async with httpx.AsyncClient(backend=TrioBackend()) as client:
- ...
-
-trio.run(main)
-```
-
-!!! important
- `trio` must be installed to import and use the `TrioBackend`.
-
-## FAQ
-
-### When should I use an `AsyncClient`?
-
-You should use an `AsyncClient` whenever you are inside an *async environment*.
-
-In particular, using `httpx.get()` or `httpx.Client()` in an async environment is **not** supported. There are several technical reasons to this, but the rationale is that you shouldn't be doing blocking-style network calls in the context of an event loop (for more discussion, see [#179](https://github.com/encode/httpx/issues/179)).
-
-For example, this won't work:
-
-```python
-import asyncio
-import httpx
-
-async def main():
- r = httpx.get("https://example.org")
-
-asyncio.run(main())
-```
-
-Instead, you should use an `AsyncClient`:
-
-```python
-import asyncio
-import httpx
-
-async def main():
- async with httpx.AsyncClient() as client:
- r = await client.get("https://example.org")
-
-asyncio.run(main())
-```
-
-If you *need* to make synchronous requests, or otherwise run into issues related to sync usage, you should probably consider using a regular threaded client such as [Requests](https://github.com/psf/requests) instead.
## Advanced Usage
### requests.Session
+
The HTTPX equivalent of `requests.Session` is `httpx.Client`.
```python
session = requests.Session(**kwargs)
```
-is equivalent to
+is generally equivalent to
```python
-with httpx.Client(**kwargs) as client:
- ...
+client = httpx.Client(**kwargs)
```
More detailed documentation and usage of `Client` can be found in [Advanced Usage](advanced.md).
<em>A next-generation HTTP client for Python.</em>
</div>
-!!! warning
- This project should be considered as an "alpha" release. It is substantially
- API complete, but there are still some areas that need more work.
+HTTPX is an asynchronous HTTP client, that supports HTTP/2 and HTTP/1.1.
+
+It can be used in high-performance async web frameworks, using either asyncio
+or trio, and is able to support making large numbers of requests concurrently.
+
+!!! note
+ The 0.8 release switched HTTPX into focusing exclusively on the async
+ client. It is possible that we'll look at re-introducing a sync API at a
+ later date.
---
Let's get started...
+!!! note
+ The standard Python REPL does not allow top-level async statements.
+
+ To run async examples directly you'll probably want to either use `ipython`,
+ or use Python 3.8 with `python -m asyncio`.
+
```python
>>> import httpx
->>> r = httpx.get('https://www.example.org/')
+>>> r = await httpx.get('https://www.example.org/')
>>> r
<Response [200 OK]>
>>> r.status_code
## Features
-HTTPX builds on the well-established usability of `requests`, and gives you:
+HTTPX is a high performance asynchronous HTTP client, that builds on the
+well-established usability of `requests`, and gives you:
-* A requests-compatible API.
+* A broadly requests-compatible API.
* HTTP/2 and HTTP/1.1 support.
-* Support for [issuing HTTP requests in parallel](parallel.md). *(Coming soon)*
-* Standard synchronous interface, but [with `async`/`await` support if you need it](async.md).
-* Ability to [make requests directly to WSGI or ASGI applications](advanced.md#calling-into-python-web-apps).
+* Ability to [make requests directly to ASGI applications](advanced.md#calling-into-python-web-apps).
* Strict timeouts everywhere.
* Fully type annotated.
* 100% test coverage.
* Automatic Content Decoding
* Unicode Response Bodies
* Multipart File Uploads
-* HTTP(S) Proxy Support *(TODO)*
+* HTTP(S) Proxy Support
* Connection Timeouts
* Streaming Downloads
* .netrc Support
For a run-through of all the basics, head over to the [QuickStart](quickstart.md).
-For more advanced topics, see the [Advanced Usage](advanced.md) section, or
-the specific topics on making [Parallel Requests](parallel.md) or using the
-[Async Client](async.md).
+For more advanced topics, see the [Advanced Usage](advanced.md) section.
The [Developer Interface](api.md) provides a comprehensive API reference.
+++ /dev/null
-# Parallel Requests
-
-!!! warning
- This page documents some proposed functionality that is not yet released.
- See [pull request #52](https://github.com/encode/httpx/pull/52) for the
- first-pass of an implementation.
-
-HTTPX allows you to make HTTP requests in parallel in a highly efficient way,
-using async under the hood, while still presenting a standard threaded interface.
-
-This has the huge benefit of allowing you to efficiently make parallel HTTP
-requests without having to switch out to using async all the way through.
-
-## Making Parallel Requests
-
-Let's make two outgoing HTTP requests in parallel:
-
-```python
->>> with httpx.parallel() as parallel:
->>> pending_one = parallel.get('https://example.com/1')
->>> pending_two = parallel.get('https://example.com/2')
->>> response_one = pending_one.get_response()
->>> response_two = pending_two.get_response()
-```
-
-If we're making lots of outgoing requests, we might not want to deal with the
-responses sequentially, but rather deal with each response that comes back
-as soon as it's available:
-
-```python
->>> with httpx.parallel() as parallel:
->>> for counter in range(1, 10):
->>> parallel.get(f'https://example.com/{counter}')
->>> while parallel.has_pending_responses:
->>> r = parallel.next_response()
-```
-
-## Exceptions and Cancellations
-
-The style of using `parallel` blocks ensures that you'll always have a well-defined exception and cancellation behaviors. Request exceptions are only ever
-raised when calling either `get_response` or `next_response` and any pending
-requests are canceled on exiting the block.
-
-## Parallel requests with a Client
-
-You can also call `parallel()` from a client instance, which allows you to
-control the authentication or dispatch behavior for all requests within the
-block.
-
-```python
->>> with httpx.Client() as client:
-... with client.parallel() as parallel:
-... ...
-```
-
-## Async parallel requests
-
-If you're working within an async framework, then you'll want to use a fully
-async API for making requests.
-
-```python
->>> async with httpx.AsyncClient() as client:
-... async with client.parallel() as parallel:
-... pending_one = await parallel.get('https://example.com/1')
-... pending_two = await parallel.get('https://example.com/2')
-... response_one = await pending_one.get_response()
-... response_two = await pending_two.get_response()
-```
-
-See [the Async Client documentation](async.md) for more details.
# QuickStart
!!! note
- This page closely follows the layout of the `requests` QuickStart documentation.
- The `httpx` library is designed to be API compatible with `requests` wherever
- possible.
+ The standard Python REPL does not allow top-level async statements.
+
+ To run async examples directly you'll probably want to either use `ipython`,
+ or use Python 3.8 with `python -m asyncio`.
First, start by importing HTTPX:
Now, let’s try to get a webpage.
```python
->>> r = httpx.get('https://httpbin.org/get')
+>>> r = await httpx.get('https://httpbin.org/get')
>>> r
<Response [200 OK]>
```
Similarly, to make an HTTP POST request:
```python
->>> r = httpx.post('https://httpbin.org/post', data={'key': 'value'})
+>>> r = await httpx.post('https://httpbin.org/post', data={'key': 'value'})
```
The PUT, DELETE, HEAD, and OPTIONS requests all follow the same style:
```python
->>> r = httpx.put('https://httpbin.org/put', data={'key': 'value'})
->>> r = httpx.delete('https://httpbin.org/delete')
->>> r = httpx.head('https://httpbin.org/get')
->>> r = httpx.options('https://httpbin.org/get')
+>>> r = await httpx.put('https://httpbin.org/put', data={'key': 'value'})
+>>> r = await httpx.delete('https://httpbin.org/delete')
+>>> r = await httpx.head('https://httpbin.org/get')
+>>> r = await httpx.options('https://httpbin.org/get')
```
## Passing Parameters in URLs
```python
>>> params = {'key1': 'value1', 'key2': 'value2'}
->>> r = httpx.get('https://httpbin.org/get', params=params)
+>>> r = await httpx.get('https://httpbin.org/get', params=params)
```
To see how the values get encoding into the URL string, we can inspect the
```python
>>> params = {'key1': 'value1', 'key2': ['value2', 'value3']}
->>> r = httpx.get('https://httpbin.org/get', params=params)
+>>> r = await httpx.get('https://httpbin.org/get', params=params)
>>> r.url
URL('https://httpbin.org/get?key1=value1&key2=value2&key2=value3')
```
HTTPX will automatically handle decoding the response content into Unicode text.
```python
->>> r = httpx.get('https://www.example.org/')
+>>> r = await httpx.get('https://www.example.org/')
>>> r.text
'<!doctype html>\n<html>\n<head>\n<title>Example Domain</title>...'
```
Often Web API responses will be encoded as JSON.
```python
->>> r = httpx.get('https://api.github.com/events')
+>>> r = await httpx.get('https://api.github.com/events')
>>> r.json()
[{u'repository': {u'open_issues': 0, u'url': 'https://github.com/...' ... }}]
```
```python
>>> url = 'http://httpbin.org/headers'
>>> headers = {'user-agent': 'my-app/0.0.1'}
->>> r = httpx.get(url, headers=headers)
+>>> r = await httpx.get(url, headers=headers)
```
## Sending Form Encoded Data
```python
>>> data = {'key1': 'value1', 'key2': 'value2'}
->>> r = httpx.post("https://httpbin.org/post", data=data)
+>>> r = await httpx.post("https://httpbin.org/post", data=data)
>>> print(r.text)
{
...
```python
>>> data = {'key1': ['value1', 'value2']}
->>> r = httpx.post("https://httpbin.org/post", data=data)
+>>> r = await httpx.post("https://httpbin.org/post", data=data)
>>> print(r.text)
{
...
```python
>>> files = {'upload-file': open('report.xls', 'rb')}
->>> r = httpx.post("https://httpbin.org/post", files=files)
+>>> r = await httpx.post("https://httpbin.org/post", files=files)
>>> print(r.text)
{
...
```python
>>> files = {'upload-file': ('report.xls', open('report.xls', 'rb'), 'application/vnd.ms-excel')}
->>> r = httpx.post("https://httpbin.org/post", files=files)
+>>> r = await httpx.post("https://httpbin.org/post", files=files)
>>> print(r.text)
{
...
```python
>>> data = {'integer': 123, 'boolean': True, 'list': ['a', 'b', 'c']}
->>> r = httpx.post("https://httpbin.org/post", json=data)
+>>> r = await httpx.post("https://httpbin.org/post", json=data)
>>> print(r.text)
{
...
We can inspect the HTTP status code of the response:
```python
->>> r = httpx.get('https://httpbin.org/get')
+>>> r = await httpx.get('https://httpbin.org/get')
>>> r.status_code
200
```
We can raise an exception for any Client or Server error responses (4xx or 5xx status codes):
```python
->>> not_found = httpx.get('https://httpbin.org/status/404')
+>>> not_found = await httpx.get('https://httpbin.org/status/404')
>>> not_found.status_code
404
>>> not_found.raise_for_status()
Any cookies that are set on the response can be easily accessed:
```python
->>> r = httpx.get('http://httpbin.org/cookies/set?chocolate=chip', allow_redirects=False)
+>>> r = await httpx.get('http://httpbin.org/cookies/set?chocolate=chip', allow_redirects=False)
>>> r.cookies['chocolate']
'chip'
```
```python
>>> cookies = {"peanut": "butter"}
->>> r = httpx.get('http://httpbin.org/cookies', cookies=cookies)
+>>> r = await httpx.get('http://httpbin.org/cookies', cookies=cookies)
>>> r.json()
{'cookies': {'peanut': 'butter'}}
```
>>> cookies = httpx.Cookies()
>>> cookies.set('cookie_on_domain', 'hello, there!', domain='httpbin.org')
>>> cookies.set('cookie_off_domain', 'nope.', domain='example.org')
->>> r = httpx.get('http://httpbin.org/cookies', cookies=cookies)
+>>> r = await httpx.get('http://httpbin.org/cookies', cookies=cookies)
>>> r.json()
{'cookies': {'cookie_on_domain': 'hello, there!'}}
```
For example, GitHub redirects all HTTP requests to HTTPS.
```python
->>> r = httpx.get('http://github.com/')
+>>> r = await httpx.get('http://github.com/')
>>> r.url
URL('https://github.com/')
>>> r.status_code
You can modify the default redirection handling with the allow_redirects parameter:
```python
->>> r = httpx.get('http://github.com/', allow_redirects=False)
+>>> r = await httpx.get('http://github.com/', allow_redirects=False)
>>> r.status_code
301
>>> r.history
If you’re making a `HEAD` request, you can use this to enable redirection:
```python
->>> r = httpx.head('http://github.com/', allow_redirects=True)
+>>> r = await httpx.head('http://github.com/', allow_redirects=True)
>>> r.url
'https://github.com/'
>>> r.history
value to be more or less strict:
```python
->>> httpx.get('https://github.com/', timeout=0.001)
+>>> await httpx.get('https://github.com/', timeout=0.001)
```
For advanced timeout management, see [Timeout fine-tuning](https://www.encode.io/httpx/advanced/#timeout-fine-tuning).
functions:
```python
->>> httpx.get("https://example.com", auth=("my_user", "password123"))
+>>> await httpx.get("https://example.com", auth=("my_user", "password123"))
```
To provide credentials for Digest authentication you'll need to instantiate
```python
>>> auth = httpx.DigestAuth("my_user", "password123")
->>> httpx.get("https://example.com", auth=auth)
+>>> await httpx.get("https://example.com", auth=auth)
<Response [200 OK]>
```
from .__version__ import __description__, __title__, __version__
from .api import delete, get, head, options, patch, post, put, request
-from .client import AsyncClient, Client
+from .client import Client
from .concurrency.asyncio import AsyncioBackend
from .concurrency.base import (
BaseBackgroundManager,
TimeoutTypes,
VerifyTypes,
)
-from .dispatch.base import AsyncDispatcher, Dispatcher
+from .dispatch.base import Dispatcher
from .dispatch.connection import HTTPConnection
from .dispatch.connection_pool import ConnectionPool
from .dispatch.proxy_http import HTTPProxy, HTTPProxyMode
from .middleware.digest_auth import DigestAuth
from .models import (
URL,
- AsyncRequest,
- AsyncRequestData,
- AsyncResponse,
- AsyncResponseContent,
AuthTypes,
Cookies,
CookieTypes,
"patch",
"put",
"request",
- "AsyncClient",
"Client",
"AsyncioBackend",
"USER_AGENT",
"Timeout",
"TooManyRedirects",
"WriteTimeout",
- "AsyncDispatcher",
"BaseSocketStream",
"ConcurrencyBackend",
"Dispatcher",
"TimeoutTypes",
"HTTPVersionTypes",
"HTTPVersionConfig",
- "AsyncRequest",
- "AsyncRequestData",
- "AsyncResponse",
- "AsyncResponseContent",
"AuthTypes",
"Cookies",
"CookieTypes",
import typing
from .client import Client
-from .config import CertTypes, TimeoutTypes, VerifyTypes
+from .config import DEFAULT_TIMEOUT_CONFIG, CertTypes, TimeoutTypes, VerifyTypes
from .models import (
AuthTypes,
CookieTypes,
)
-def request(
+async def request(
method: str,
url: URLTypes,
*,
headers: HeaderTypes = None,
cookies: CookieTypes = None,
auth: AuthTypes = None,
- timeout: TimeoutTypes = None,
+ timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
allow_redirects: bool = True,
verify: VerifyTypes = True,
cert: CertTypes = None,
stream: bool = False,
- trust_env: bool = None,
+ trust_env: bool = True,
) -> Response:
"""
Sends an HTTP request.
```
>>> import httpx
- >>> response = httpx.request('GET', 'https://httpbin.org/get')
+ >>> response = await httpx.request('GET', 'https://httpbin.org/get')
>>> response
<Response [200 OK]>
```
"""
- with Client(http_versions=["HTTP/1.1"]) as client:
- return client.request(
+ async with Client(
+ http_versions=["HTTP/1.1"],
+ cert=cert,
+ verify=verify,
+ timeout=timeout,
+ trust_env=trust_env,
+ ) as client:
+ return await client.request(
method=method,
url=url,
data=data,
stream=stream,
auth=auth,
allow_redirects=allow_redirects,
- cert=cert,
- verify=verify,
- timeout=timeout,
- trust_env=trust_env,
)
-def get(
+async def get(
url: URLTypes,
*,
params: QueryParamTypes = None,
allow_redirects: bool = True,
cert: CertTypes = None,
verify: VerifyTypes = True,
- timeout: TimeoutTypes = None,
- trust_env: bool = None,
+ timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
+ trust_env: bool = True,
) -> Response:
"""
Sends a `GET` request.
Note that the `data`, `files`, and `json` parameters are not available on
this function, as `GET` requests should not include a request body.
"""
- return request(
+ return await request(
"GET",
url,
params=params,
)
-def options(
+async def options(
url: URLTypes,
*,
params: QueryParamTypes = None,
allow_redirects: bool = True,
cert: CertTypes = None,
verify: VerifyTypes = True,
- timeout: TimeoutTypes = None,
- trust_env: bool = None,
+ timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
+ trust_env: bool = True,
) -> Response:
"""
Sends an `OPTIONS` request.
Note that the `data`, `files`, and `json` parameters are not available on
this function, as `OPTIONS` requests should not include a request body.
"""
- return request(
+ return await request(
"OPTIONS",
url,
params=params,
)
-def head(
+async def head(
url: URLTypes,
*,
params: QueryParamTypes = None,
cookies: CookieTypes = None,
stream: bool = False,
auth: AuthTypes = None,
- allow_redirects: bool = False, # Note: Differs to usual default.
+ allow_redirects: bool = False, # Note: Differs to usual default.
cert: CertTypes = None,
verify: VerifyTypes = True,
- timeout: TimeoutTypes = None,
- trust_env: bool = None,
+ timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
+ trust_env: bool = True,
) -> Response:
"""
Sends a `HEAD` request.
`HEAD` method also differs from the other cases in that `allow_redirects`
defaults to `False`.
"""
- return request(
+ return await request(
"HEAD",
url,
params=params,
)
-def post(
+async def post(
url: URLTypes,
*,
data: RequestData = None,
allow_redirects: bool = True,
cert: CertTypes = None,
verify: VerifyTypes = True,
- timeout: TimeoutTypes = None,
- trust_env: bool = None,
+ timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
+ trust_env: bool = True,
) -> Response:
"""
Sends a `POST` request.
**Parameters**: See `httpx.request`.
"""
- return request(
+ return await request(
"POST",
url,
data=data,
)
-def put(
+async def put(
url: URLTypes,
*,
data: RequestData = None,
allow_redirects: bool = True,
cert: CertTypes = None,
verify: VerifyTypes = True,
- timeout: TimeoutTypes = None,
- trust_env: bool = None,
+ timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
+ trust_env: bool = True,
) -> Response:
"""
Sends a `PUT` request.
**Parameters**: See `httpx.request`.
"""
- return request(
+ return await request(
"PUT",
url,
data=data,
)
-def patch(
+async def patch(
url: URLTypes,
*,
data: RequestData = None,
allow_redirects: bool = True,
cert: CertTypes = None,
verify: VerifyTypes = True,
- timeout: TimeoutTypes = None,
- trust_env: bool = None,
+ timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
+ trust_env: bool = True,
) -> Response:
"""
Sends a `PATCH` request.
**Parameters**: See `httpx.request`.
"""
- return request(
+ return await request(
"PATCH",
url,
data=data,
)
-def delete(
+async def delete(
url: URLTypes,
*,
params: QueryParamTypes = None,
allow_redirects: bool = True,
cert: CertTypes = None,
verify: VerifyTypes = True,
- timeout: TimeoutTypes = None,
- trust_env: bool = None,
+ timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
+ trust_env: bool = True,
) -> Response:
"""
Sends a `DELETE` request.
Note that the `data`, `files`, and `json` parameters are not available on
this function, as `DELETE` requests should not include a request body.
"""
- return request(
+ return await request(
"DELETE",
url,
params=params,
import functools
-import inspect
import netrc
import typing
from types import TracebackType
VerifyTypes,
)
from .dispatch.asgi import ASGIDispatch
-from .dispatch.base import AsyncDispatcher, Dispatcher
+from .dispatch.base import Dispatcher
from .dispatch.connection_pool import ConnectionPool
from .dispatch.proxy_http import HTTPProxy
-from .dispatch.threaded import ThreadedDispatcher
-from .dispatch.wsgi import WSGIDispatch
from .exceptions import HTTPError, InvalidURL
from .middleware.base import BaseMiddleware
from .middleware.basic_auth import BasicAuthMiddleware
from .middleware.redirect import RedirectMiddleware
from .models import (
URL,
- AsyncRequest,
- AsyncRequestData,
- AsyncResponse,
- AsyncResponseContent,
AuthTypes,
Cookies,
CookieTypes,
ProxiesTypes,
QueryParams,
QueryParamTypes,
+ Request,
RequestData,
RequestFiles,
Response,
- ResponseContent,
URLTypes,
)
from .utils import ElapsedTimer, get_environment_proxies, get_logger, get_netrc
logger = get_logger(__name__)
-class BaseClient:
+class Client:
+ """
+ An HTTP client, with connection pooling, HTTP/2, redirects, cookie persistence, etc.
+
+ Usage:
+
+ ```
+ >>> client = httpx.Client()
+ >>> response = client.get('https://example.org')
+ ```
+
+ **Parameters:**
+
+ * **auth** - *(optional)* An authentication class to use when sending
+ requests.
+ * **params** - *(optional)* Query parameters to include in request URLs, as
+ a string, dictionary, or list of two-tuples.
+ * **headers** - *(optional)* Dictionary of HTTP headers to include when
+ sending requests.
+ * **cookies** - *(optional)* Dictionary of Cookie items to include when
+ sending requests.
+ * **verify** - *(optional)* SSL certificates (a.k.a CA bundle) used to
+ verify the identity of requested hosts. Either `True` (default CA bundle),
+ a path to an SSL certificate file, or `False` (disable verification).
+ * **cert** - *(optional)* An SSL certificate used by the requested host
+ to authenticate the client. Either a path to an SSL certificate file, or
+ two-tuple of (certificate file, key file), or a three-tuple of (certificate
+ file, key file, password).
+ * **http_versions** - *(optional)* A list of strings of HTTP protocol
+ versions to use when sending requests. eg. `http_versions=["HTTP/1.1"]`
+ * **proxies** - *(optional)* A dictionary mapping HTTP protocols to proxy
+ URLs.
+ * **timeout** - *(optional)* The timeout configuration to use when sending
+ requests.
+ * **pool_limits** - *(optional)* The connection pool configuration to use
+ when determining the maximum number of concurrently open HTTP connections.
+ * **max_redirects** - *(optional)* The maximum number of redirect responses
+ that should be followed.
+ * **base_url** - *(optional)* A URL to use as the base when building
+ request URLs.
+ * **dispatch** - *(optional)* A dispatch class to use for sending requests
+ over the network.
+ * **app** - *(optional)* An ASGI application to send requests to,
+ rather than sending actual network requests.
+ * **backend** - *(optional)* A concurrency backend to use when issuing
+ async requests.
+ * **trust_env** - *(optional)* Enables or disables usage of environment
+ variables for configuration.
+ * **uds** - *(optional)* A path to a Unix domain socket to connect through.
+ """
+
def __init__(
self,
*,
pool_limits: PoolLimits = DEFAULT_POOL_LIMITS,
max_redirects: int = DEFAULT_MAX_REDIRECTS,
base_url: URLTypes = None,
- dispatch: typing.Union[AsyncDispatcher, Dispatcher] = None,
+ dispatch: Dispatcher = None,
app: typing.Callable = None,
backend: ConcurrencyBackend = None,
trust_env: bool = True,
if backend is None:
backend = AsyncioBackend()
- self.check_concurrency_backend(backend)
-
if app is not None:
- param_count = len(inspect.signature(app).parameters)
- assert param_count in (2, 3)
- if param_count == 2:
- dispatch = WSGIDispatch(app=app)
- else:
- dispatch = ASGIDispatch(app=app, backend=backend)
+ dispatch = ASGIDispatch(app=app, backend=backend)
self.trust_env = True if trust_env is None else trust_env
if dispatch is None:
- async_dispatch: AsyncDispatcher = ConnectionPool(
+ dispatch = ConnectionPool(
verify=verify,
cert=cert,
timeout=timeout,
trust_env=self.trust_env,
uds=uds,
)
- elif isinstance(dispatch, Dispatcher):
- async_dispatch = ThreadedDispatcher(dispatch, backend)
- else:
- async_dispatch = dispatch
if base_url is None:
self.base_url = URL("", allow_relative=True)
self._headers = Headers(headers)
self._cookies = Cookies(cookies)
self.max_redirects = max_redirects
- self.dispatch = async_dispatch
+ self.dispatch = dispatch
self.concurrency_backend = backend
if proxies is None and trust_env:
proxies = typing.cast(ProxiesTypes, get_environment_proxies())
- self.proxies: typing.Dict[str, AsyncDispatcher] = _proxies_to_dispatchers(
+ self.proxies: typing.Dict[str, Dispatcher] = _proxies_to_dispatchers(
proxies,
verify=verify,
cert=cert,
def params(self, params: QueryParamTypes) -> None:
self._params = QueryParams(params)
- def check_concurrency_backend(self, backend: ConcurrencyBackend) -> None:
- pass # pragma: no cover
-
def merge_url(self, url: URLTypes) -> URL:
url = self.base_url.join(relative_url=url)
if url.scheme == "http" and hstspreload.in_hsts_preload(url.host):
async def _get_response(
self,
- request: AsyncRequest,
+ request: Request,
*,
stream: bool = False,
auth: AuthTypes = None,
cert: CertTypes = None,
timeout: TimeoutTypes = None,
trust_env: bool = None,
- ) -> AsyncResponse:
+ ) -> Response:
if request.url.scheme not in ("http", "https"):
raise InvalidURL('URL scheme must be "http" or "https".')
dispatch = self._dispatcher_for_request(request, self.proxies)
- async def get_response(request: AsyncRequest) -> AsyncResponse:
+ async def get_response(request: Request) -> Response:
try:
with ElapsedTimer() as timer:
response = await dispatch.send(
request, verify=verify, cert=cert, timeout=timeout
)
response.elapsed = timer.elapsed
+ response.request = request
except HTTPError as exc:
# Add the original request to any HTTPError unless
# there'a already a request attached in the case of
return await get_response(request)
def _get_auth_middleware(
- self, request: AsyncRequest, trust_env: bool, auth: AuthTypes = None
+ self, request: Request, trust_env: bool, auth: AuthTypes = None
) -> typing.Optional[BaseMiddleware]:
if isinstance(auth, tuple):
return BasicAuthMiddleware(username=auth[0], password=auth[1])
if auth is not None:
raise TypeError(
'When specified, "auth" must be a (username, password) tuple or '
- "a callable with signature (AsyncRequest) -> AsyncRequest "
+ "a callable with signature (Request) -> Request "
f"(got {auth!r})"
)
return get_netrc()
def _dispatcher_for_request(
- self, request: AsyncRequest, proxies: typing.Dict[str, AsyncDispatcher]
- ) -> AsyncDispatcher:
- """Gets the AsyncDispatcher instance that should be used for a given Request"""
+ self, request: Request, proxies: typing.Dict[str, Dispatcher]
+ ) -> Dispatcher:
+ """Gets the Dispatcher instance that should be used for a given Request"""
if proxies:
url = request.url
is_default_port = (url.scheme == "http" and url.port == 80) or (
method: str,
url: URLTypes,
*,
- data: AsyncRequestData = None,
+ data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
params: QueryParamTypes = None,
headers: HeaderTypes = None,
cookies: CookieTypes = None,
- ) -> AsyncRequest:
+ ) -> Request:
"""
Build and return a request instance.
"""
headers = self.merge_headers(headers)
cookies = self.merge_cookies(cookies)
params = self.merge_queryparams(params)
- return AsyncRequest(
+ return Request(
method,
url,
data=data,
cookies=cookies,
)
-
-class AsyncClient(BaseClient):
async def get(
self,
url: URLTypes,
verify: VerifyTypes = None,
timeout: TimeoutTypes = None,
trust_env: bool = None,
- ) -> AsyncResponse:
+ ) -> Response:
return await self.request(
"GET",
url,
verify: VerifyTypes = None,
timeout: TimeoutTypes = None,
trust_env: bool = None,
- ) -> AsyncResponse:
+ ) -> Response:
return await self.request(
"OPTIONS",
url,
verify: VerifyTypes = None,
timeout: TimeoutTypes = None,
trust_env: bool = None,
- ) -> AsyncResponse:
+ ) -> Response:
return await self.request(
"HEAD",
url,
self,
url: URLTypes,
*,
- data: AsyncRequestData = None,
+ data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
params: QueryParamTypes = None,
verify: VerifyTypes = None,
timeout: TimeoutTypes = None,
trust_env: bool = None,
- ) -> AsyncResponse:
+ ) -> Response:
return await self.request(
"POST",
url,
self,
url: URLTypes,
*,
- data: AsyncRequestData = None,
+ data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
params: QueryParamTypes = None,
verify: VerifyTypes = None,
timeout: TimeoutTypes = None,
trust_env: bool = None,
- ) -> AsyncResponse:
+ ) -> Response:
return await self.request(
"PUT",
url,
self,
url: URLTypes,
*,
- data: AsyncRequestData = None,
+ data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
params: QueryParamTypes = None,
verify: VerifyTypes = None,
timeout: TimeoutTypes = None,
trust_env: bool = None,
- ) -> AsyncResponse:
+ ) -> Response:
return await self.request(
"PATCH",
url,
verify: VerifyTypes = None,
timeout: TimeoutTypes = None,
trust_env: bool = None,
- ) -> AsyncResponse:
+ ) -> Response:
return await self.request(
"DELETE",
url,
method: str,
url: URLTypes,
*,
- data: AsyncRequestData = None,
+ data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
params: QueryParamTypes = None,
verify: VerifyTypes = None,
timeout: TimeoutTypes = None,
trust_env: bool = None,
- ) -> AsyncResponse:
+ ) -> Response:
request = self.build_request(
method=method,
url=url,
async def send(
self,
- request: AsyncRequest,
+ request: Request,
*,
stream: bool = False,
auth: AuthTypes = None,
cert: CertTypes = None,
timeout: TimeoutTypes = None,
trust_env: bool = None,
- ) -> AsyncResponse:
+ ) -> Response:
return await self._get_response(
request=request,
stream=stream,
async def close(self) -> None:
await self.dispatch.close()
- async def __aenter__(self) -> "AsyncClient":
+ async def __aenter__(self) -> "Client":
return self
async def __aexit__(
await self.close()
-class Client(BaseClient):
- """
- An HTTP client, with connection pooling, HTTP/2, redirects, cookie persistence, etc.
-
- Usage:
-
- ```
- >>> client = httpx.Client()
- >>> response = client.get('https://example.org')
- ```
-
- **Parameters:**
-
- * **auth** - *(optional)* An authentication class to use when sending
- requests.
- * **params** - *(optional)* Query parameters to include in request URLs, as
- a string, dictionary, or list of two-tuples.
- * **headers** - *(optional)* Dictionary of HTTP headers to include when
- sending requests.
- * **cookies** - *(optional)* Dictionary of Cookie items to include when
- sending requests.
- * **verify** - *(optional)* SSL certificates (a.k.a CA bundle) used to
- verify the identity of requested hosts. Either `True` (default CA bundle),
- a path to an SSL certificate file, or `False` (disable verification).
- * **cert** - *(optional)* An SSL certificate used by the requested host
- to authenticate the client. Either a path to an SSL certificate file, or
- two-tuple of (certificate file, key file), or a three-tuple of (certificate
- file, key file, password).
- * **http_versions** - *(optional)* A list of strings of HTTP protocol
- versions to use when sending requests. eg. `http_versions=["HTTP/1.1"]`
- * **proxies** - *(optional)* A dictionary mapping HTTP protocols to proxy
- URLs.
- * **timeout** - *(optional)* The timeout configuration to use when sending
- requests.
- * **pool_limits** - *(optional)* The connection pool configuration to use
- when determining the maximum number of concurrently open HTTP connections.
- * **max_redirects** - *(optional)* The maximum number of redirect responses
- that should be followed.
- * **base_url** - *(optional)* A URL to use as the base when building
- request URLs.
- * **dispatch** - *(optional)* A dispatch class to use for sending requests
- over the network.
- * **app** - *(optional)* A WSGI or ASGI application to send requests to,
- rather than sending actual network requests.
- * **backend** - *(optional)* A concurrency backend to use when issuing
- async requests.
- * **trust_env** - *(optional)* Enables or disables usage of environment
- variables for configuration.
- * **uds** - *(optional)* A path to a Unix domain socket to connect through.
- """
-
- def check_concurrency_backend(self, backend: ConcurrencyBackend) -> None:
- # Iterating over response content allocates an async environment on each step.
- # This is relatively cheap on asyncio, but cannot be guaranteed for all
- # concurrency backends.
- # The sync client performs I/O on its own, so it doesn't need to support
- # arbitrary concurrency backends.
- # Therefore, we keep the `backend` parameter (for testing/mocking), but require
- # that the concurrency backend relies on asyncio.
-
- if isinstance(backend, AsyncioBackend):
- return
-
- if hasattr(backend, "loop"):
- # Most likely a proxy class.
- return
-
- raise ValueError("'Client' only supports asyncio-based concurrency backends")
-
- def _async_request_data(
- self, data: RequestData = None
- ) -> typing.Optional[AsyncRequestData]:
- """
- If the request data is an bytes iterator then return an async bytes
- iterator onto the request data.
- """
- if data is None or isinstance(data, (str, bytes, dict)):
- return data
-
- # Coerce an iterator into an async iterator, with each item in the
- # iteration running as a thread-pooled operation.
- assert hasattr(data, "__iter__")
- return self.concurrency_backend.iterate_in_threadpool(data)
-
- def _sync_data(self, data: AsyncResponseContent) -> ResponseContent:
- if isinstance(data, bytes):
- return data
-
- # Coerce an async iterator into an iterator, with each item in the
- # iteration run within the event loop.
- assert hasattr(data, "__aiter__")
- return self.concurrency_backend.iterate(data)
-
- def request(
- self,
- method: str,
- url: URLTypes,
- *,
- data: RequestData = None,
- files: RequestFiles = None,
- json: typing.Any = None,
- params: QueryParamTypes = None,
- headers: HeaderTypes = None,
- cookies: CookieTypes = None,
- stream: bool = False,
- auth: AuthTypes = None,
- allow_redirects: bool = True,
- verify: VerifyTypes = None,
- cert: CertTypes = None,
- timeout: TimeoutTypes = None,
- trust_env: bool = None,
- ) -> Response:
- """
- Sends an HTTP request.
-
- **Parameters:**
-
- * **method** - HTTP method for the new `Request` object: `GET`, `OPTIONS`,
- `HEAD`, `POST`, `PUT`, `PATCH`, or `DELETE`.
- * **url** - URL for the new `Request` object.
- * **data** - *(optional)* Data to include in the body of the request, as a
- dictionary
- * **files** - *(optional)* A dictionary of upload files to include in the
- body of the request.
- * **json** - *(optional)* A JSON serializable object to include in the body
- of the request.
- * **params** - *(optional)* Query parameters to include in the URL, as a
- string, dictionary, or list of two-tuples.
- * **headers** - *(optional)* Dictionary of HTTP headers to include on the
- request.
- * **cookies** - *(optional)* Dictionary of Cookie items to include in the
- request.
- * **stream** - *(optional)* Enable/disable streaming responses.
- * **auth** - *(optional)* An authentication class to use when sending the
- request.
- * **allow_redirects** - *(optional)* Enables or disables HTTP redirects.
- * **verify** - *(optional)* SSL certificates (a.k.a CA bundle) used to
- verify the identity of requested hosts. Either `True` (default CA bundle),
- a path to an SSL certificate file, or `False` (disable verification).
- * **cert** - *(optional)* An SSL certificate used by the requested host
- to authenticate the client. Either a path to an SSL certificate file, or
- two-tuple of (certificate file, key file), or a three-tuple of (certificate
- file, key file, password).
- * **timeout** - *(optional)* The timeout configuration to use when sending
- the request.
- * **trust_env** - *(optional)* Enables or disables usage of environment
- variables for configuration.
-
- **Returns:** `Response`
-
- Usage:
-
- ```
- >>> import httpx
- >>> client = httpx.Client()
- >>> response = client.request('GET', 'https://httpbin.org/get')
- >>> response
- <Response [200 OK]>
- ```
- """
- request = self.build_request(
- method=method,
- url=url,
- data=self._async_request_data(data),
- files=files,
- json=json,
- params=params,
- headers=headers,
- cookies=cookies,
- )
- return self.send(
- request,
- stream=stream,
- auth=auth,
- allow_redirects=allow_redirects,
- verify=verify,
- cert=cert,
- timeout=timeout,
- trust_env=trust_env,
- )
-
- def send(
- self,
- request: AsyncRequest,
- *,
- stream: bool = False,
- auth: AuthTypes = None,
- allow_redirects: bool = True,
- verify: VerifyTypes = None,
- cert: CertTypes = None,
- timeout: TimeoutTypes = None,
- trust_env: bool = None,
- ) -> Response:
- """
- Sends a request over the network, returning a response.
- """
- concurrency_backend = self.concurrency_backend
-
- coroutine = self._get_response
- args = [request]
- kwargs = {
- "stream": True,
- "auth": auth,
- "allow_redirects": allow_redirects,
- "verify": verify,
- "cert": cert,
- "timeout": timeout,
- "trust_env": trust_env,
- }
- async_response = concurrency_backend.run(coroutine, *args, **kwargs)
-
- content = getattr(
- async_response, "_raw_content", getattr(async_response, "_raw_stream", None)
- )
-
- sync_content = self._sync_data(content)
-
- def sync_on_close() -> None:
- nonlocal concurrency_backend, async_response
- concurrency_backend.run(async_response.on_close)
-
- response = Response(
- status_code=async_response.status_code,
- http_version=async_response.http_version,
- headers=async_response.headers,
- content=sync_content,
- on_close=sync_on_close,
- request=async_response.request,
- history=async_response.history,
- elapsed=async_response.elapsed,
- )
- if not stream:
- try:
- response.read()
- finally:
- response.close()
- return response
-
- def get(
- self,
- url: URLTypes,
- *,
- params: QueryParamTypes = None,
- headers: HeaderTypes = None,
- cookies: CookieTypes = None,
- stream: bool = False,
- auth: AuthTypes = None,
- allow_redirects: bool = True,
- cert: CertTypes = None,
- verify: VerifyTypes = None,
- timeout: TimeoutTypes = None,
- trust_env: bool = None,
- ) -> Response:
- """
- Sends a `GET` request.
-
- **Parameters**: See `Client.request`.
-
- Note that the `data`, `files`, and `json` parameters are not available on
- this function, as `GET` requests should not include a request body.
- """
- return self.request(
- "GET",
- url,
- params=params,
- headers=headers,
- cookies=cookies,
- stream=stream,
- auth=auth,
- allow_redirects=allow_redirects,
- verify=verify,
- cert=cert,
- timeout=timeout,
- trust_env=trust_env,
- )
-
- def options(
- self,
- url: URLTypes,
- *,
- params: QueryParamTypes = None,
- headers: HeaderTypes = None,
- cookies: CookieTypes = None,
- stream: bool = False,
- auth: AuthTypes = None,
- allow_redirects: bool = True,
- cert: CertTypes = None,
- verify: VerifyTypes = None,
- timeout: TimeoutTypes = None,
- trust_env: bool = None,
- ) -> Response:
- """
- Sends an `OPTIONS` request.
-
- **Parameters**: See `Client.request`.
-
- Note that the `data`, `files`, and `json` parameters are not available on
- this function, as `OPTIONS` requests should not include a request body.
- """
- return self.request(
- "OPTIONS",
- url,
- params=params,
- headers=headers,
- cookies=cookies,
- stream=stream,
- auth=auth,
- allow_redirects=allow_redirects,
- verify=verify,
- cert=cert,
- timeout=timeout,
- trust_env=trust_env,
- )
-
- def head(
- self,
- url: URLTypes,
- *,
- params: QueryParamTypes = None,
- headers: HeaderTypes = None,
- cookies: CookieTypes = None,
- stream: bool = False,
- auth: AuthTypes = None,
- allow_redirects: bool = False, # NOTE: Differs to usual default.
- cert: CertTypes = None,
- verify: VerifyTypes = None,
- timeout: TimeoutTypes = None,
- trust_env: bool = None,
- ) -> Response:
- """
- Sends a `HEAD` request.
-
- **Parameters**: See `Client.request`.
-
- Note that the `data`, `files`, and `json` parameters are not available on
- this function, as `HEAD` requests should not include a request body. The
- `HEAD` method also differs from the other cases in that `allow_redirects`
- defaults to `False`.
- """
- return self.request(
- "HEAD",
- url,
- params=params,
- headers=headers,
- cookies=cookies,
- stream=stream,
- auth=auth,
- allow_redirects=allow_redirects,
- verify=verify,
- cert=cert,
- timeout=timeout,
- trust_env=trust_env,
- )
-
- def post(
- self,
- url: URLTypes,
- *,
- data: RequestData = None,
- files: RequestFiles = None,
- json: typing.Any = None,
- params: QueryParamTypes = None,
- headers: HeaderTypes = None,
- cookies: CookieTypes = None,
- stream: bool = False,
- auth: AuthTypes = None,
- allow_redirects: bool = True,
- cert: CertTypes = None,
- verify: VerifyTypes = None,
- timeout: TimeoutTypes = None,
- trust_env: bool = None,
- ) -> Response:
- """
- Sends a `POST` request.
-
- **Parameters**: See `Client.request`.
- """
- return self.request(
- "POST",
- url,
- data=data,
- files=files,
- json=json,
- params=params,
- headers=headers,
- cookies=cookies,
- stream=stream,
- auth=auth,
- allow_redirects=allow_redirects,
- verify=verify,
- cert=cert,
- timeout=timeout,
- trust_env=trust_env,
- )
-
- def put(
- self,
- url: URLTypes,
- *,
- data: RequestData = None,
- files: RequestFiles = None,
- json: typing.Any = None,
- params: QueryParamTypes = None,
- headers: HeaderTypes = None,
- cookies: CookieTypes = None,
- stream: bool = False,
- auth: AuthTypes = None,
- allow_redirects: bool = True,
- cert: CertTypes = None,
- verify: VerifyTypes = None,
- timeout: TimeoutTypes = None,
- trust_env: bool = None,
- ) -> Response:
- """
- Sends a `PUT` request.
-
- **Parameters**: See `Client.request`.
- """
- return self.request(
- "PUT",
- url,
- data=data,
- files=files,
- json=json,
- params=params,
- headers=headers,
- cookies=cookies,
- stream=stream,
- auth=auth,
- allow_redirects=allow_redirects,
- verify=verify,
- cert=cert,
- timeout=timeout,
- trust_env=trust_env,
- )
-
- def patch(
- self,
- url: URLTypes,
- *,
- data: RequestData = None,
- files: RequestFiles = None,
- json: typing.Any = None,
- params: QueryParamTypes = None,
- headers: HeaderTypes = None,
- cookies: CookieTypes = None,
- stream: bool = False,
- auth: AuthTypes = None,
- allow_redirects: bool = True,
- cert: CertTypes = None,
- verify: VerifyTypes = None,
- timeout: TimeoutTypes = None,
- trust_env: bool = None,
- ) -> Response:
- """
- Sends a `PATCH` request.
-
- **Parameters**: See `Client.request`.
- """
- return self.request(
- "PATCH",
- url,
- data=data,
- files=files,
- json=json,
- params=params,
- headers=headers,
- cookies=cookies,
- stream=stream,
- auth=auth,
- allow_redirects=allow_redirects,
- verify=verify,
- cert=cert,
- timeout=timeout,
- trust_env=trust_env,
- )
-
- def delete(
- self,
- url: URLTypes,
- *,
- params: QueryParamTypes = None,
- headers: HeaderTypes = None,
- cookies: CookieTypes = None,
- stream: bool = False,
- auth: AuthTypes = None,
- allow_redirects: bool = True,
- cert: CertTypes = None,
- verify: VerifyTypes = None,
- timeout: TimeoutTypes = None,
- trust_env: bool = None,
- ) -> Response:
- """
- Sends a `DELETE` request.
-
- **Parameters**: See `Client.request`.
- """
- return self.request(
- "DELETE",
- url,
- params=params,
- headers=headers,
- cookies=cookies,
- stream=stream,
- auth=auth,
- allow_redirects=allow_redirects,
- verify=verify,
- cert=cert,
- timeout=timeout,
- trust_env=trust_env,
- )
-
- def close(self) -> None:
- """
- Close any open connections in the connection pool.
- """
- coroutine = self.dispatch.close
- self.concurrency_backend.run(coroutine)
-
- def __enter__(self) -> "Client":
- return self
-
- def __exit__(
- self,
- exc_type: typing.Type[BaseException] = None,
- exc_value: BaseException = None,
- traceback: TracebackType = None,
- ) -> None:
- self.close()
-
-
def _proxies_to_dispatchers(
proxies: typing.Optional[ProxiesTypes],
verify: VerifyTypes,
pool_limits: PoolLimits,
backend: ConcurrencyBackend,
trust_env: bool,
-) -> typing.Dict[str, AsyncDispatcher]:
- def _proxy_from_url(url: URLTypes) -> AsyncDispatcher:
+) -> typing.Dict[str, Dispatcher]:
+ def _proxy_from_url(url: URLTypes) -> Dispatcher:
nonlocal verify, cert, timeout, http_versions, pool_limits, backend, trust_env
url = URL(url)
if url.scheme in ("http", "https"):
return {}
elif isinstance(proxies, (str, URL)):
return {"all": _proxy_from_url(proxies)}
- elif isinstance(proxies, AsyncDispatcher):
+ elif isinstance(proxies, Dispatcher):
return {"all": proxies}
else:
new_proxies = {}
details of making the HTTP request and receiving the response.
"""
from .asgi import ASGIDispatch
-from .wsgi import WSGIDispatch
-__all__ = ["ASGIDispatch", "WSGIDispatch"]
+__all__ = ["ASGIDispatch"]
from ..concurrency.asyncio import AsyncioBackend
from ..concurrency.base import ConcurrencyBackend
from ..config import CertTypes, TimeoutTypes, VerifyTypes
-from ..models import AsyncRequest, AsyncResponse
+from ..models import Request, Response
from ..utils import MessageLoggerASGIMiddleware, get_logger
-from .base import AsyncDispatcher
+from .base import Dispatcher
logger = get_logger(__name__)
-class ASGIDispatch(AsyncDispatcher):
+class ASGIDispatch(Dispatcher):
"""
A custom dispatcher that handles sending requests directly to an ASGI app.
async def send(
self,
- request: AsyncRequest,
+ request: Request,
verify: VerifyTypes = None,
cert: CertTypes = None,
timeout: TimeoutTypes = None,
- ) -> AsyncResponse:
+ ) -> Response:
scope = {
"type": "http",
if app_exc is not None and self.raise_app_exceptions:
raise app_exc
- return AsyncResponse(
+ return Response(
status_code=status_code,
http_version="HTTP/1.1",
headers=headers,
from ..config import CertTypes, TimeoutTypes, VerifyTypes
from ..models import (
- AsyncRequest,
- AsyncRequestData,
- AsyncResponse,
HeaderTypes,
QueryParamTypes,
Request,
)
-class AsyncDispatcher:
- """
- Base class for async dispatcher classes, that handle sending the request.
-
- Stubs out the interface, as well as providing a `.request()` convenience
- implementation, to make it easy to use or test stand-alone dispatchers,
- without requiring a complete `Client` instance.
- """
-
- async def request(
- self,
- method: str,
- url: URLTypes,
- *,
- data: AsyncRequestData = b"",
- params: QueryParamTypes = None,
- headers: HeaderTypes = None,
- verify: VerifyTypes = None,
- cert: CertTypes = None,
- timeout: TimeoutTypes = None,
- ) -> AsyncResponse:
- request = AsyncRequest(method, url, data=data, params=params, headers=headers)
- return await self.send(request, verify=verify, cert=cert, timeout=timeout)
-
- async def send(
- self,
- request: AsyncRequest,
- verify: VerifyTypes = None,
- cert: CertTypes = None,
- timeout: TimeoutTypes = None,
- ) -> AsyncResponse:
- raise NotImplementedError() # pragma: nocover
-
- async def close(self) -> None:
- pass # pragma: nocover
-
- async def __aenter__(self) -> "AsyncDispatcher":
- return self
-
- async def __aexit__(
- self,
- exc_type: typing.Type[BaseException] = None,
- exc_value: BaseException = None,
- traceback: TracebackType = None,
- ) -> None:
- await self.close()
-
-
class Dispatcher:
"""
- Base class for synchronous dispatcher classes, that handle sending the request.
+ Base class for dispatcher classes, that handle sending the request.
Stubs out the interface, as well as providing a `.request()` convenience
implementation, to make it easy to use or test stand-alone dispatchers,
without requiring a complete `Client` instance.
"""
- def request(
+ async def request(
self,
method: str,
url: URLTypes,
timeout: TimeoutTypes = None,
) -> Response:
request = Request(method, url, data=data, params=params, headers=headers)
- return self.send(request, verify=verify, cert=cert, timeout=timeout)
+ return await self.send(request, verify=verify, cert=cert, timeout=timeout)
- def send(
+ async def send(
self,
request: Request,
verify: VerifyTypes = None,
) -> Response:
raise NotImplementedError() # pragma: nocover
- def close(self) -> None:
+ async def close(self) -> None:
pass # pragma: nocover
- def __enter__(self) -> "Dispatcher":
+ async def __aenter__(self) -> "Dispatcher":
return self
- def __exit__(
+ async def __aexit__(
self,
exc_type: typing.Type[BaseException] = None,
exc_value: BaseException = None,
traceback: TracebackType = None,
) -> None:
- self.close()
+ await self.close()
TimeoutTypes,
VerifyTypes,
)
-from ..models import AsyncRequest, AsyncResponse, Origin
+from ..models import Origin, Request, Response
from ..utils import get_logger
-from .base import AsyncDispatcher
+from .base import Dispatcher
from .http2 import HTTP2Connection
from .http11 import HTTP11Connection
logger = get_logger(__name__)
-class HTTPConnection(AsyncDispatcher):
+class HTTPConnection(Dispatcher):
def __init__(
self,
origin: typing.Union[str, Origin],
async def send(
self,
- request: AsyncRequest,
+ request: Request,
verify: VerifyTypes = None,
cert: CertTypes = None,
timeout: TimeoutTypes = None,
- ) -> AsyncResponse:
+ ) -> Response:
if self.h11_connection is None and self.h2_connection is None:
await self.connect(verify=verify, cert=cert, timeout=timeout)
TimeoutTypes,
VerifyTypes,
)
-from ..models import AsyncRequest, AsyncResponse, Origin
+from ..models import Origin, Request, Response
from ..utils import get_logger
-from .base import AsyncDispatcher
+from .base import Dispatcher
from .connection import HTTPConnection
CONNECTIONS_DICT = typing.Dict[Origin, typing.List[HTTPConnection]]
return len(self.all)
-class ConnectionPool(AsyncDispatcher):
+class ConnectionPool(Dispatcher):
def __init__(
self,
*,
async def send(
self,
- request: AsyncRequest,
+ request: Request,
verify: VerifyTypes = None,
cert: CertTypes = None,
timeout: TimeoutTypes = None,
- ) -> AsyncResponse:
+ ) -> Response:
connection = await self.acquire_connection(origin=request.url.origin)
try:
response = await connection.send(
from ..concurrency.base import BaseSocketStream, ConcurrencyBackend, TimeoutFlag
from ..config import TimeoutConfig, TimeoutTypes
from ..exceptions import ConnectionClosed, ProtocolError
-from ..models import AsyncRequest, AsyncResponse
+from ..models import Request, Response
from ..utils import get_logger
H11Event = typing.Union[
self.h11_state = h11.Connection(our_role=h11.CLIENT)
self.timeout_flag = TimeoutFlag()
- async def send(
- self, request: AsyncRequest, timeout: TimeoutTypes = None
- ) -> AsyncResponse:
+ async def send(self, request: Request, timeout: TimeoutTypes = None) -> Response:
timeout = None if timeout is None else TimeoutConfig(timeout)
await self._send_request(request, timeout)
http_version, status_code, headers = await self._receive_response(timeout)
content = self._receive_response_data(timeout)
- return AsyncResponse(
+ return Response(
status_code=status_code,
http_version=http_version,
headers=headers,
await self.stream.close()
async def _send_request(
- self, request: AsyncRequest, timeout: TimeoutConfig = None
+ self, request: Request, timeout: TimeoutConfig = None
) -> None:
"""
Send the request method, URL, and headers to the network.
)
from ..config import TimeoutConfig, TimeoutTypes
from ..exceptions import ProtocolError
-from ..models import AsyncRequest, AsyncResponse
+from ..models import Request, Response
from ..utils import get_logger
logger = get_logger(__name__)
self.initialized = False
self.window_update_received = {} # type: typing.Dict[int, BaseEvent]
- async def send(
- self, request: AsyncRequest, timeout: TimeoutTypes = None
- ) -> AsyncResponse:
+ async def send(self, request: Request, timeout: TimeoutTypes = None) -> Response:
timeout = None if timeout is None else TimeoutConfig(timeout)
# Start sending the request.
content = self.body_iter(stream_id, timeout)
on_close = functools.partial(self.response_closed, stream_id=stream_id)
- return AsyncResponse(
+ return Response(
status_code=status_code,
http_version="HTTP/2",
headers=headers,
self.initialized = True
async def send_headers(
- self, request: AsyncRequest, timeout: TimeoutConfig = None
+ self, request: Request, timeout: TimeoutConfig = None
) -> int:
stream_id = self.h2_state.get_next_available_stream_id()
headers = [
)
from ..exceptions import ProxyError
from ..middleware.basic_auth import build_basic_auth_header
-from ..models import (
- URL,
- AsyncRequest,
- AsyncResponse,
- Headers,
- HeaderTypes,
- Origin,
- URLTypes,
-)
+from ..models import URL, Headers, HeaderTypes, Origin, Request, Response, URLTypes
from ..utils import get_logger
from .connection import HTTPConnection
from .connection_pool import ConnectionPool
"""Creates an HTTPConnection by setting up a TCP tunnel"""
proxy_headers = self.proxy_headers.copy()
proxy_headers.setdefault("Accept", "*/*")
- proxy_request = AsyncRequest(
+ proxy_request = Request(
method="CONNECT", url=self.proxy_url.copy_with(), headers=proxy_headers
)
proxy_request.url.full_path = f"{origin.host}:{origin.port}"
async def send(
self,
- request: AsyncRequest,
+ request: Request,
verify: VerifyTypes = None,
cert: CertTypes = None,
timeout: TimeoutTypes = None,
- ) -> AsyncResponse:
+ ) -> Response:
if self.should_forward_origin(request.url.origin):
# Change the request to have the target URL
+++ /dev/null
-from ..concurrency.base import ConcurrencyBackend
-from ..config import CertTypes, TimeoutTypes, VerifyTypes
-from ..models import (
- AsyncRequest,
- AsyncRequestData,
- AsyncResponse,
- AsyncResponseContent,
- Request,
- RequestData,
- Response,
- ResponseContent,
-)
-from .base import AsyncDispatcher, Dispatcher
-
-
-class ThreadedDispatcher(AsyncDispatcher):
- """
- The ThreadedDispatcher class is used to mediate between the Client
- (which always uses async under the hood), and a synchronous `Dispatch`
- class.
- """
-
- def __init__(self, dispatch: Dispatcher, backend: ConcurrencyBackend) -> None:
- self.sync_dispatcher = dispatch
- self.backend = backend
-
- async def send(
- self,
- request: AsyncRequest,
- verify: VerifyTypes = None,
- cert: CertTypes = None,
- timeout: TimeoutTypes = None,
- ) -> AsyncResponse:
- concurrency_backend = self.backend
-
- data = getattr(request, "content", getattr(request, "content_aiter", None))
- sync_data = self._sync_request_data(data)
-
- sync_request = Request(
- method=request.method,
- url=request.url,
- headers=request.headers,
- data=sync_data,
- )
-
- func = self.sync_dispatcher.send
- kwargs = {
- "request": sync_request,
- "verify": verify,
- "cert": cert,
- "timeout": timeout,
- }
- sync_response = await self.backend.run_in_threadpool(func, **kwargs)
- assert isinstance(sync_response, Response)
-
- content = getattr(
- sync_response, "_raw_content", getattr(sync_response, "_raw_stream", None)
- )
-
- async_content = self._async_response_content(content)
-
- async def async_on_close() -> None:
- nonlocal concurrency_backend, sync_response
- await concurrency_backend.run_in_threadpool(sync_response.close)
-
- return AsyncResponse(
- status_code=sync_response.status_code,
- http_version=sync_response.http_version,
- headers=sync_response.headers,
- content=async_content,
- on_close=async_on_close,
- request=request,
- history=sync_response.history,
- )
-
- async def close(self) -> None:
- """
- The `.close()` method runs the `Dispatcher.close()` within a threadpool,
- so as not to block the async event loop.
- """
- func = self.sync_dispatcher.close
- await self.backend.run_in_threadpool(func)
-
- def _async_response_content(self, content: ResponseContent) -> AsyncResponseContent:
- if isinstance(content, bytes):
- return content
-
- # Coerce an async iterator into an iterator, with each item in the
- # iteration run within the event loop.
- assert hasattr(content, "__iter__")
- return self.backend.iterate_in_threadpool(content)
-
- def _sync_request_data(self, data: AsyncRequestData) -> RequestData:
- if isinstance(data, bytes):
- return data
-
- return self.backend.iterate(data)
+++ /dev/null
-import io
-import typing
-
-from ..config import CertTypes, TimeoutTypes, VerifyTypes
-from ..models import Request, Response
-from .base import Dispatcher
-
-
-class WSGIDispatch(Dispatcher):
- """
- A custom dispatcher that handles sending requests directly to an ASGI app.
-
- The simplest way to use this functionality is to use the `app`argument.
- This will automatically infer if 'app' is a WSGI or an ASGI application,
- and will setup an appropriate dispatch class:
-
- ```
- client = httpx.Client(app=app)
- ```
-
- Alternatively, you can setup the dispatch instance explicitly.
- This allows you to include any additional configuration arguments specific
- to the WSGIDispatch class:
-
- ```
- dispatch = httpx.WSGIDispatch(
- app=app,
- script_name="/submount",
- remote_addr="1.2.3.4"
- )
- client = httpx.Client(dispatch=dispatch)
-
-
- Arguments:
-
- * `app` - The ASGI application.
- * `raise_app_exceptions` - Boolean indicating if exceptions in the application
- should be raised. Default to `True`. Can be set to `False` for use cases
- such as testing the content of a client 500 response.
- * `script_name` - The root path on which the ASGI application should be mounted.
- * `remote_addr` - A string indicating the client IP of incoming requests.
- ```
- """
-
- def __init__(
- self,
- app: typing.Callable,
- raise_app_exceptions: bool = True,
- script_name: str = "",
- remote_addr: str = "127.0.0.1",
- ) -> None:
- self.app = app
- self.raise_app_exceptions = raise_app_exceptions
- self.script_name = script_name
- self.remote_addr = remote_addr
-
- def send(
- self,
- request: Request,
- verify: VerifyTypes = None,
- cert: CertTypes = None,
- timeout: TimeoutTypes = None,
- ) -> Response:
- environ = {
- "wsgi.version": (1, 0),
- "wsgi.url_scheme": request.url.scheme,
- "wsgi.input": BodyStream(request.stream()),
- "wsgi.errors": io.BytesIO(),
- "wsgi.multithread": True,
- "wsgi.multiprocess": False,
- "wsgi.run_once": False,
- "REQUEST_METHOD": request.method,
- "SCRIPT_NAME": self.script_name,
- "PATH_INFO": request.url.path,
- "QUERY_STRING": request.url.query,
- "SERVER_NAME": request.url.host,
- "SERVER_PORT": str(request.url.port),
- "REMOTE_ADDR": self.remote_addr,
- }
- for key, value in request.headers.items():
- key = key.upper().replace("-", "_")
- if key not in ("CONTENT_TYPE", "CONTENT_LENGTH"):
- key = "HTTP_" + key
- environ[key] = value
-
- seen_status = None
- seen_response_headers = None
- seen_exc_info = None
-
- def start_response(
- status: str, response_headers: list, exc_info: typing.Any = None
- ) -> None:
- nonlocal seen_status, seen_response_headers, seen_exc_info
- seen_status = status
- seen_response_headers = response_headers
- seen_exc_info = exc_info
-
- result = self.app(environ, start_response)
-
- assert seen_status is not None
- assert seen_response_headers is not None
- if seen_exc_info and self.raise_app_exceptions:
- raise seen_exc_info[1]
-
- return Response(
- status_code=int(seen_status.split()[0]),
- http_version="HTTP/1.1",
- headers=seen_response_headers,
- content=(chunk for chunk in result),
- on_close=getattr(result, "close", None),
- )
-
-
-class BodyStream(io.RawIOBase):
- def __init__(self, iterator: typing.Iterator[bytes]) -> None:
- self._iterator = iterator
- self._buffer = b""
- self._closed = False
-
- def read(self, size: int = -1) -> bytes:
- if self._closed:
- return b""
-
- if size == -1:
- return self.readall()
-
- try:
- while len(self._buffer) < size:
- self._buffer += next(self._iterator)
- except StopIteration:
- self._closed = True
- return self._buffer
-
- output = self._buffer[:size]
- self._buffer = self._buffer[size:]
- return output
-
- def readall(self) -> bytes:
- if self._closed:
- raise OSError("Stream closed") # pragma: nocover
-
- for chunk in self._iterator:
- self._buffer += chunk
-
- self._closed = True
- return self._buffer
-
- def readinto(self, b: bytearray) -> typing.Optional[int]: # pragma: nocover
- output = self.read(len(b))
- count = len(output)
- b[:count] = output
- return count
-
- def write(self, b: bytes) -> int:
- raise OSError("Operation not supported") # pragma: nocover
-
- def fileno(self) -> int:
- raise OSError("Operation not supported") # pragma: nocover
-
- def seek(self, offset: int, whence: int = 0) -> int:
- raise OSError("Operation not supported") # pragma: nocover
-
- def truncate(self, size: int = None) -> int:
- raise OSError("Operation not supported") # pragma: nocover
import typing
if typing.TYPE_CHECKING:
- from .models import BaseRequest, BaseResponse # pragma: nocover
+ from .models import Request, Response # pragma: nocover
class HTTPError(Exception):
"""
def __init__(
- self,
- *args: typing.Any,
- request: "BaseRequest" = None,
- response: "BaseResponse" = None,
+ self, *args: typing.Any, request: "Request" = None, response: "Response" = None
) -> None:
self.response = response
self.request = request or getattr(self.response, "request", None)
import typing
-from ..models import AsyncRequest, AsyncResponse
+from ..models import Request, Response
class BaseMiddleware:
async def __call__(
- self, request: AsyncRequest, get_response: typing.Callable
- ) -> AsyncResponse:
+ self, request: Request, get_response: typing.Callable
+ ) -> Response:
raise NotImplementedError # pragma: no cover
import typing
from base64 import b64encode
-from ..models import AsyncRequest, AsyncResponse
+from ..models import Request, Response
from ..utils import to_bytes
from .base import BaseMiddleware
self.authorization_header = build_basic_auth_header(username, password)
async def __call__(
- self, request: AsyncRequest, get_response: typing.Callable
- ) -> AsyncResponse:
+ self, request: Request, get_response: typing.Callable
+ ) -> Response:
request.headers["Authorization"] = self.authorization_header
return await get_response(request)
import typing
-from ..models import AsyncRequest, AsyncResponse
+from ..models import Request, Response
from .base import BaseMiddleware
class CustomAuthMiddleware(BaseMiddleware):
- def __init__(self, auth: typing.Callable[[AsyncRequest], AsyncRequest]):
+ def __init__(self, auth: typing.Callable[[Request], Request]):
self.auth = auth
async def __call__(
- self, request: AsyncRequest, get_response: typing.Callable
- ) -> AsyncResponse:
+ self, request: Request, get_response: typing.Callable
+ ) -> Response:
request = self.auth(request)
return await get_response(request)
from urllib.request import parse_http_list
from ..exceptions import ProtocolError
-from ..models import AsyncRequest, AsyncResponse, StatusCode
+from ..models import Request, Response, StatusCode
from ..utils import to_bytes, to_str, unquote
from .base import BaseMiddleware
self.password = to_bytes(password)
async def __call__(
- self, request: AsyncRequest, get_response: typing.Callable
- ) -> AsyncResponse:
+ self, request: Request, get_response: typing.Callable
+ ) -> Response:
response = await get_response(request)
if not (
StatusCode.is_client_error(response.status_code)
return await get_response(request)
def _build_auth_header(
- self, request: AsyncRequest, challenge: "DigestAuthChallenge"
+ self, request: Request, challenge: "DigestAuthChallenge"
) -> str:
hash_func = self.ALGORITHM_TO_HASH_FUNCTION[challenge.algorithm]
from ..config import DEFAULT_MAX_REDIRECTS
from ..exceptions import RedirectBodyUnavailable, RedirectLoop, TooManyRedirects
-from ..models import URL, AsyncRequest, AsyncResponse, Cookies, Headers
+from ..models import URL, Cookies, Headers, Request, Response
from ..status_codes import codes
from .base import BaseMiddleware
self.allow_redirects = allow_redirects
self.max_redirects = max_redirects
self.cookies = cookies
- self.history: typing.List[AsyncResponse] = []
+ self.history: typing.List[Response] = []
async def __call__(
- self, request: AsyncRequest, get_response: typing.Callable
- ) -> AsyncResponse:
+ self, request: Request, get_response: typing.Callable
+ ) -> Response:
if len(self.history) > self.max_redirects:
raise TooManyRedirects()
if request.url in (response.url for response in self.history):
response.call_next = functools.partial(self, next_request, get_response)
return response
- def build_redirect_request(
- self, request: AsyncRequest, response: AsyncResponse
- ) -> AsyncRequest:
+ def build_redirect_request(self, request: Request, response: Response) -> Request:
method = self.redirect_method(request, response)
url = self.redirect_url(request, response)
headers = self.redirect_headers(request, url, method) # TODO: merge headers?
content = self.redirect_content(request, method)
cookies = Cookies(self.cookies)
- return AsyncRequest(
+ return Request(
method=method, url=url, headers=headers, data=content, cookies=cookies
)
- def redirect_method(self, request: AsyncRequest, response: AsyncResponse) -> str:
+ def redirect_method(self, request: Request, response: Response) -> str:
"""
When being redirected we may want to change the method of the request
based on certain specs or browser behavior.
return method
- def redirect_url(self, request: AsyncRequest, response: AsyncResponse) -> URL:
+ def redirect_url(self, request: Request, response: Response) -> URL:
"""
Return the URL for the redirect to follow.
"""
return url
- def redirect_headers(self, request: AsyncRequest, url: URL, method: str) -> Headers:
+ def redirect_headers(self, request: Request, url: URL, method: str) -> Headers:
"""
Return the headers that should be used for the redirect request.
"""
return headers
- def redirect_content(self, request: AsyncRequest, method: str) -> bytes:
+ def redirect_content(self, request: Request, method: str) -> bytes:
"""
Return the body that should be used for the redirect request.
"""
if typing.TYPE_CHECKING: # pragma: no cover
from .middleware.base import BaseMiddleware # noqa: F401
- from .dispatch.base import AsyncDispatcher # noqa: F401
+ from .dispatch.base import Dispatcher # noqa: F401
PrimitiveData = typing.Optional[typing.Union[str, int, float, bool]]
AuthTypes = typing.Union[
typing.Tuple[typing.Union[str, bytes], typing.Union[str, bytes]],
- typing.Callable[["AsyncRequest"], "AsyncRequest"],
+ typing.Callable[["Request"], "Request"],
"BaseMiddleware",
]
ProxiesTypes = typing.Union[
- URLTypes,
- "AsyncDispatcher",
- typing.Dict[URLTypes, typing.Union[URLTypes, "AsyncDispatcher"]],
+ URLTypes, "Dispatcher", typing.Dict[URLTypes, typing.Union[URLTypes, "Dispatcher"]]
]
-AsyncRequestData = typing.Union[dict, str, bytes, typing.AsyncIterator[bytes]]
-
-RequestData = typing.Union[dict, str, bytes, typing.Iterator[bytes]]
+RequestData = typing.Union[dict, str, bytes, typing.AsyncIterator[bytes]]
RequestFiles = typing.Dict[
str,
],
]
-AsyncResponseContent = typing.Union[bytes, typing.AsyncIterator[bytes]]
-
-ResponseContent = typing.Union[bytes, typing.Iterator[bytes]]
+ResponseContent = typing.Union[bytes, typing.AsyncIterator[bytes]]
class URL:
return f"{class_name}({as_list!r}{encoding_str})"
-class BaseRequest:
+class Request:
def __init__(
self,
method: str,
params: QueryParamTypes = None,
headers: HeaderTypes = None,
cookies: CookieTypes = None,
+ data: RequestData = None,
+ files: RequestFiles = None,
+ json: typing.Any = None,
):
self.method = method.upper()
self.url = URL(url, params=params)
if cookies:
self._cookies = Cookies(cookies)
self._cookies.set_cookie_header(self)
+ if data is None or isinstance(data, dict):
+ content, content_type = self.encode_data(data, files, json)
+ self.is_streaming = False
+ self.content = content
+ if content_type:
+ self.headers["Content-Type"] = content_type
+ elif isinstance(data, (str, bytes)):
+ data = data.encode("utf-8") if isinstance(data, str) else data
+ self.is_streaming = False
+ self.content = data
+ else:
+ assert hasattr(data, "__aiter__")
+ self.is_streaming = True
+ self.content_aiter = data
+ self.prepare()
def encode_data(
self, data: dict = None, files: RequestFiles = None, json: typing.Any = None
url = str(self.url)
return f"<{class_name}({self.method!r}, {url!r})>"
-
-class AsyncRequest(BaseRequest):
- def __init__(
- self,
- method: str,
- url: typing.Union[str, URL],
- *,
- params: QueryParamTypes = None,
- headers: HeaderTypes = None,
- cookies: CookieTypes = None,
- data: AsyncRequestData = None,
- files: RequestFiles = None,
- json: typing.Any = None,
- ):
- super().__init__(
- method=method, url=url, params=params, headers=headers, cookies=cookies
- )
-
- if data is None or isinstance(data, dict):
- content, content_type = self.encode_data(data, files, json)
- self.is_streaming = False
- self.content = content
- if content_type:
- self.headers["Content-Type"] = content_type
- elif isinstance(data, (str, bytes)):
- data = data.encode("utf-8") if isinstance(data, str) else data
- self.is_streaming = False
- self.content = data
- else:
- assert hasattr(data, "__aiter__")
- self.is_streaming = True
- self.content_aiter = data
- self.prepare()
-
async def read(self) -> bytes:
"""
Read and return the response content.
yield self.content
-class Request(BaseRequest):
- def __init__(
- self,
- method: str,
- url: typing.Union[str, URL],
- *,
- params: QueryParamTypes = None,
- headers: HeaderTypes = None,
- cookies: CookieTypes = None,
- data: RequestData = None,
- files: RequestFiles = None,
- json: typing.Any = None,
- ):
- super().__init__(
- method=method, url=url, params=params, headers=headers, cookies=cookies
- )
-
- if data is None or isinstance(data, dict):
- content, content_type = self.encode_data(data, files, json)
- self.is_streaming = False
- self.content = content
- if content_type:
- self.headers["Content-Type"] = content_type
- elif isinstance(data, (str, bytes)):
- data = data.encode("utf-8") if isinstance(data, str) else data
- self.is_streaming = False
- self.content = data
- else:
- assert hasattr(data, "__iter__")
- self.is_streaming = True
- self.content_iter = data
-
- self.prepare()
-
- def read(self) -> bytes:
- if not hasattr(self, "content"):
- self.content = b"".join([part for part in self.stream()])
- return self.content
-
- def stream(self) -> typing.Iterator[bytes]:
- if self.is_streaming:
- for part in self.content_iter:
- yield part
- elif self.content:
- yield self.content
-
-
-class BaseResponse:
+class Response:
def __init__(
self,
status_code: int,
*,
http_version: str = None,
headers: HeaderTypes = None,
- request: BaseRequest = None,
+ content: ResponseContent = None,
on_close: typing.Callable = None,
+ request: Request = None,
+ history: typing.List["Response"] = None,
elapsed: datetime.timedelta = None,
):
self.status_code = status_code
self.elapsed = datetime.timedelta(0) if elapsed is None else elapsed
self.call_next: typing.Optional[typing.Callable] = None
+ self.history = [] if history is None else list(history)
+
+ if content is None or isinstance(content, bytes):
+ self.is_closed = True
+ self.is_stream_consumed = True
+ self._raw_content = content or b""
+ else:
+ self.is_closed = False
+ self.is_stream_consumed = False
+ self._raw_stream = content
+
@property
def reason_phrase(self) -> str:
return StatusCode.get_reason_phrase(self.status_code)
def __repr__(self) -> str:
return f"<Response [{self.status_code} {self.reason_phrase}]>"
-
-class AsyncResponse(BaseResponse):
- def __init__(
- self,
- status_code: int,
- *,
- http_version: str = None,
- headers: HeaderTypes = None,
- content: AsyncResponseContent = None,
- on_close: typing.Callable = None,
- request: AsyncRequest = None,
- history: typing.List["BaseResponse"] = None,
- elapsed: datetime.timedelta = None,
- ):
- super().__init__(
- status_code=status_code,
- http_version=http_version,
- headers=headers,
- request=request,
- on_close=on_close,
- elapsed=elapsed,
- )
-
- self.history = [] if history is None else list(history)
-
- if content is None or isinstance(content, bytes):
- self.is_closed = True
- self.is_stream_consumed = True
- self._raw_content = content or b""
- else:
- self.is_closed = False
- self.is_stream_consumed = False
- self._raw_stream = content
-
async def read(self) -> bytes:
"""
Read and return the response content.
yield part
await self.close()
- async def next(self) -> "AsyncResponse":
+ async def next(self) -> "Response":
"""
Get the next response from a redirect response.
"""
await self.on_close()
-class Response(BaseResponse):
- def __init__(
- self,
- status_code: int,
- *,
- http_version: str = None,
- headers: HeaderTypes = None,
- content: ResponseContent = None,
- on_close: typing.Callable = None,
- request: Request = None,
- history: typing.List["BaseResponse"] = None,
- elapsed: datetime.timedelta = None,
- ):
- super().__init__(
- status_code=status_code,
- http_version=http_version,
- headers=headers,
- request=request,
- on_close=on_close,
- elapsed=elapsed,
- )
-
- self.history = [] if history is None else list(history)
-
- if content is None or isinstance(content, bytes):
- self.is_closed = True
- self.is_stream_consumed = True
- self._raw_content = content or b""
- else:
- self.is_closed = False
- self.is_stream_consumed = False
- self._raw_stream = content
-
- def read(self) -> bytes:
- """
- Read and return the response content.
- """
- if not hasattr(self, "_content"):
- self._content = b"".join([part for part in self.stream()])
- return self._content
-
- def stream(self) -> typing.Iterator[bytes]:
- """
- A byte-iterator over the decoded response content.
- This allows us to handle gzip, deflate, and brotli encoded responses.
- """
- if hasattr(self, "_content"):
- yield self._content
- else:
- for chunk in self.raw():
- yield self.decoder.decode(chunk)
- yield self.decoder.flush()
-
- def stream_text(self) -> typing.Iterator[str]:
- """
- A str-iterator over the decoded response content
- that handles both gzip, deflate, etc but also detects the content's
- string encoding.
- """
- decoder = TextDecoder(encoding=self.charset_encoding)
- for chunk in self.stream():
- yield decoder.decode(chunk)
- yield decoder.flush()
-
- def raw(self) -> typing.Iterator[bytes]:
- """
- A byte-iterator over the raw response content.
- """
- if hasattr(self, "_raw_content"):
- yield self._raw_content
- else:
- if self.is_stream_consumed:
- raise StreamConsumed()
- if self.is_closed:
- raise ResponseClosed()
-
- self.is_stream_consumed = True
- for part in self._raw_stream:
- yield part
- self.close()
-
- def close(self) -> None:
- """
- Close the response and release the connection.
- Automatically called if the response body is read to completion.
- """
- if not self.is_closed:
- self.is_closed = True
- if self.on_close is not None:
- self.on_close()
-
-
class Cookies(MutableMapping):
"""
HTTP Cookies, as a mutable mapping.
else:
self.jar = cookies
- def extract_cookies(self, response: BaseResponse) -> None:
+ def extract_cookies(self, response: Response) -> None:
"""
Loads any cookies based on the response `Set-Cookie` headers.
"""
self.jar.extract_cookies(urlib_response, urllib_request) # type: ignore
- def set_cookie_header(self, request: BaseRequest) -> None:
+ def set_cookie_header(self, request: Request) -> None:
"""
Sets an appropriate 'Cookie:' HTTP header on the `Request`.
"""
for use with `CookieJar` operations.
"""
- def __init__(self, request: BaseRequest) -> None:
+ def __init__(self, request: Request) -> None:
super().__init__(
url=str(request.url),
headers=dict(request.headers),
for use with `CookieJar` operations.
"""
- def __init__(self, response: BaseResponse):
+ def __init__(self, response: Response):
self.response = response
def info(self) -> email.message.Message:
- QuickStart: 'quickstart.md'
- Advanced Usage: 'advanced.md'
- Environment Variables: 'environment_variables.md'
- - Parallel Requests: 'parallel.md'
- - Async Client: 'async.md'
- Requests Compatibility: 'compatibility.md'
- Developer Interface: 'api.md'
- Contributing: 'contributing.md'
async def test_get(server, backend):
url = server.url
- async with httpx.AsyncClient(backend=backend) as client:
+ async with httpx.Client(backend=backend) as client:
response = await client.get(url)
assert response.status_code == 200
assert response.text == "Hello, world!"
async def test_build_request(server, backend):
url = server.url.copy_with(path="/echo_headers")
headers = {"Custom-header": "value"}
- async with httpx.AsyncClient(backend=backend) as client:
+ async with httpx.Client(backend=backend) as client:
request = client.build_request("GET", url)
request.headers.update(headers)
response = await client.send(request)
Verify that the client is capable of making a simple request if not given a backend.
"""
url = server.url
- async with httpx.AsyncClient() as client:
+ async with httpx.Client() as client:
response = await client.get(url)
assert response.status_code == 200
assert response.text == "Hello, world!"
async def test_post(server, backend):
url = server.url
- async with httpx.AsyncClient(backend=backend) as client:
+ async with httpx.Client(backend=backend) as client:
response = await client.post(url, data=b"Hello, world!")
assert response.status_code == 200
async def test_post_json(server, backend):
url = server.url
- async with httpx.AsyncClient(backend=backend) as client:
+ async with httpx.Client(backend=backend) as client:
response = await client.post(url, json={"text": "Hello, world!"})
assert response.status_code == 200
async def test_stream_response(server, backend):
- async with httpx.AsyncClient(backend=backend) as client:
+ async with httpx.Client(backend=backend) as client:
response = await client.request("GET", server.url, stream=True)
assert response.status_code == 200
body = await response.read()
async def test_access_content_stream_response(server, backend):
- async with httpx.AsyncClient(backend=backend) as client:
+ async with httpx.Client(backend=backend) as client:
response = await client.request("GET", server.url, stream=True)
assert response.status_code == 200
with pytest.raises(httpx.ResponseNotRead):
yield b"Hello, "
yield b"world!"
- async with httpx.AsyncClient(backend=backend) as client:
+ async with httpx.Client(backend=backend) as client:
response = await client.request("POST", server.url, data=hello_world())
assert response.status_code == 200
async def test_raise_for_status(server, backend):
- async with httpx.AsyncClient(backend=backend) as client:
+ async with httpx.Client(backend=backend) as client:
for status_code in (200, 400, 404, 500, 505):
response = await client.request(
"GET", server.url.copy_with(path=f"/status/{status_code}")
async def test_options(server, backend):
- async with httpx.AsyncClient(backend=backend) as client:
+ async with httpx.Client(backend=backend) as client:
response = await client.options(server.url)
assert response.status_code == 200
assert response.text == "Hello, world!"
async def test_head(server, backend):
- async with httpx.AsyncClient(backend=backend) as client:
+ async with httpx.Client(backend=backend) as client:
response = await client.head(server.url)
assert response.status_code == 200
assert response.text == ""
async def test_put(server, backend):
- async with httpx.AsyncClient(backend=backend) as client:
+ async with httpx.Client(backend=backend) as client:
response = await client.put(server.url, data=b"Hello, world!")
assert response.status_code == 200
async def test_patch(server, backend):
- async with httpx.AsyncClient(backend=backend) as client:
+ async with httpx.Client(backend=backend) as client:
response = await client.patch(server.url, data=b"Hello, world!")
assert response.status_code == 200
async def test_delete(server, backend):
- async with httpx.AsyncClient(backend=backend) as client:
+ async with httpx.Client(backend=backend) as client:
response = await client.delete(server.url)
assert response.status_code == 200
assert response.text == "Hello, world!"
headers = {"Expect": "100-continue"}
data = b"Echo request body"
- async with httpx.AsyncClient(backend=backend) as client:
+ async with httpx.Client(backend=backend) as client:
response = await client.post(
server.url.copy_with(path="/echo_body"), headers=headers, data=data
)
url = uds_server.url
uds = uds_server.config.uds
assert uds is not None
- async with httpx.AsyncClient(backend=backend, uds=uds) as client:
+ async with httpx.Client(backend=backend, uds=uds) as client:
response = await client.get(url)
assert response.status_code == 200
assert response.text == "Hello, world!"
from httpx import (
URL,
- AsyncDispatcher,
- AsyncRequest,
- AsyncResponse,
CertTypes,
Client,
DigestAuth,
+ Dispatcher,
ProtocolError,
+ Request,
+ Response,
TimeoutTypes,
VerifyTypes,
)
-class MockDispatch(AsyncDispatcher):
+class MockDispatch(Dispatcher):
def __init__(self, auth_header: str = "", status_code: int = 200) -> None:
self.auth_header = auth_header
self.status_code = status_code
async def send(
self,
- request: AsyncRequest,
+ request: Request,
verify: VerifyTypes = None,
cert: CertTypes = None,
timeout: TimeoutTypes = None,
- ) -> AsyncResponse:
+ ) -> Response:
headers = [("www-authenticate", self.auth_header)] if self.auth_header else []
body = json.dumps({"auth": request.headers.get("Authorization")}).encode()
- return AsyncResponse(
+ return Response(
self.status_code, headers=headers, content=body, request=request
)
-class MockDigestAuthDispatch(AsyncDispatcher):
+class MockDigestAuthDispatch(Dispatcher):
def __init__(
self,
algorithm: str = "SHA-256",
async def send(
self,
- request: AsyncRequest,
+ request: Request,
verify: VerifyTypes = None,
cert: CertTypes = None,
timeout: TimeoutTypes = None,
- ) -> AsyncResponse:
+ ) -> Response:
if self._response_count < self.send_response_after_attempt:
return self.challenge_send(request)
body = json.dumps({"auth": request.headers.get("Authorization")}).encode()
- return AsyncResponse(200, content=body, request=request)
+ return Response(200, content=body, request=request)
- def challenge_send(self, request: AsyncRequest) -> AsyncResponse:
+ def challenge_send(self, request: Request) -> Response:
self._response_count += 1
nonce = (
hashlib.sha256(os.urandom(8)).hexdigest()
headers = [
("www-authenticate", 'Digest realm="httpx@example.org", ' + challenge_str)
]
- return AsyncResponse(401, headers=headers, content=b"", request=request)
+ return Response(401, headers=headers, content=b"", request=request)
-def test_basic_auth():
+@pytest.mark.asyncio
+async def test_basic_auth():
url = "https://example.org/"
auth = ("tomchristie", "password123")
- with Client(dispatch=MockDispatch()) as client:
- response = client.get(url, auth=auth)
+ client = Client(dispatch=MockDispatch())
+ response = await client.get(url, auth=auth)
assert response.status_code == 200
assert response.json() == {"auth": "Basic dG9tY2hyaXN0aWU6cGFzc3dvcmQxMjM="}
-def test_basic_auth_in_url():
+@pytest.mark.asyncio
+async def test_basic_auth_in_url():
url = "https://tomchristie:password123@example.org/"
- with Client(dispatch=MockDispatch()) as client:
- response = client.get(url)
+ client = Client(dispatch=MockDispatch())
+ response = await client.get(url)
assert response.status_code == 200
assert response.json() == {"auth": "Basic dG9tY2hyaXN0aWU6cGFzc3dvcmQxMjM="}
-def test_basic_auth_on_session():
+@pytest.mark.asyncio
+async def test_basic_auth_on_session():
url = "https://example.org/"
auth = ("tomchristie", "password123")
- with Client(dispatch=MockDispatch(), auth=auth) as client:
- response = client.get(url)
+ client = Client(dispatch=MockDispatch(), auth=auth)
+ response = await client.get(url)
assert response.status_code == 200
assert response.json() == {"auth": "Basic dG9tY2hyaXN0aWU6cGFzc3dvcmQxMjM="}
-def test_custom_auth():
+@pytest.mark.asyncio
+async def test_custom_auth():
url = "https://example.org/"
def auth(request):
request.headers["Authorization"] = "Token 123"
return request
- with Client(dispatch=MockDispatch()) as client:
- response = client.get(url, auth=auth)
+ client = Client(dispatch=MockDispatch())
+ response = await client.get(url, auth=auth)
assert response.status_code == 200
assert response.json() == {"auth": "Token 123"}
-def test_netrc_auth():
+@pytest.mark.asyncio
+async def test_netrc_auth():
os.environ["NETRC"] = "tests/.netrc"
url = "http://netrcexample.org"
- with Client(dispatch=MockDispatch()) as client:
- response = client.get(url)
+ client = Client(dispatch=MockDispatch())
+ response = await client.get(url)
assert response.status_code == 200
assert response.json() == {
}
-def test_trust_env_auth():
+@pytest.mark.asyncio
+async def test_trust_env_auth():
os.environ["NETRC"] = "tests/.netrc"
url = "http://netrcexample.org"
- with Client(dispatch=MockDispatch()) as client:
- response = client.get(url, trust_env=False)
+ client = Client(dispatch=MockDispatch())
+ response = await client.get(url, trust_env=False)
assert response.status_code == 200
assert response.json() == {"auth": None}
- with Client(dispatch=MockDispatch(), trust_env=False) as client:
- response = client.get(url, trust_env=True)
+ client = Client(dispatch=MockDispatch(), trust_env=False)
+ response = await client.get(url, trust_env=True)
assert response.status_code == 200
assert response.json() == {
assert expected == repr(URL(url))
-def test_auth_hidden_header():
+@pytest.mark.asyncio
+async def test_auth_hidden_header():
url = "https://example.org/"
auth = ("example-username", "example-password")
- with Client(dispatch=MockDispatch()) as client:
- response = client.get(url, auth=auth)
+ client = Client(dispatch=MockDispatch())
+ response = await client.get(url, auth=auth)
assert "'authorization': '[secure]'" in str(response.request.headers)
-def test_auth_invalid_type():
+@pytest.mark.asyncio
+async def test_auth_invalid_type():
url = "https://example.org/"
- with Client(dispatch=MockDispatch(), auth="not a tuple, not a callable") as client:
- with pytest.raises(TypeError):
- client.get(url)
+ client = Client(dispatch=MockDispatch(), auth="not a tuple, not a callable")
+ with pytest.raises(TypeError):
+ await client.get(url)
-def test_digest_auth_returns_no_auth_if_no_digest_header_in_response():
+@pytest.mark.asyncio
+async def test_digest_auth_returns_no_auth_if_no_digest_header_in_response():
url = "https://example.org/"
auth = DigestAuth(username="tomchristie", password="password123")
- with Client(dispatch=MockDispatch()) as client:
- response = client.get(url, auth=auth)
+ client = Client(dispatch=MockDispatch())
+ response = await client.get(url, auth=auth)
assert response.status_code == 200
assert response.json() == {"auth": None}
-def test_digest_auth_200_response_including_digest_auth_header_is_returned_as_is():
+@pytest.mark.asyncio
+async def test_digest_auth_200_response_including_digest_auth_header():
url = "https://example.org/"
auth = DigestAuth(username="tomchristie", password="password123")
auth_header = 'Digest realm="realm@host.com",qop="auth",nonce="abc",opaque="xyz"'
- with Client(
- dispatch=MockDispatch(auth_header=auth_header, status_code=200)
- ) as client:
- response = client.get(url, auth=auth)
+ client = Client(dispatch=MockDispatch(auth_header=auth_header, status_code=200))
+ response = await client.get(url, auth=auth)
assert response.status_code == 200
assert response.json() == {"auth": None}
-def test_digest_auth_401_response_without_digest_auth_header_is_returned_as_is():
+@pytest.mark.asyncio
+async def test_digest_auth_401_response_without_digest_auth_header():
url = "https://example.org/"
auth = DigestAuth(username="tomchristie", password="password123")
- with Client(dispatch=MockDispatch(auth_header="", status_code=401)) as client:
- response = client.get(url, auth=auth)
+ client = Client(dispatch=MockDispatch(auth_header="", status_code=401))
+ response = await client.get(url, auth=auth)
assert response.status_code == 401
assert response.json() == {"auth": None}
("SHA-512-SESS", 64, 128),
],
)
-def test_digest_auth(algorithm, expected_hash_length, expected_response_length):
+@pytest.mark.asyncio
+async def test_digest_auth(algorithm, expected_hash_length, expected_response_length):
url = "https://example.org/"
auth = DigestAuth(username="tomchristie", password="password123")
- with Client(dispatch=MockDigestAuthDispatch(algorithm=algorithm)) as client:
- response = client.get(url, auth=auth)
+ client = Client(dispatch=MockDigestAuthDispatch(algorithm=algorithm))
+ response = await client.get(url, auth=auth)
assert response.status_code == 200
auth = response.json()["auth"]
assert len(digest_data["cnonce"]) == 16 + 2
-def test_digest_auth_no_specified_qop():
+@pytest.mark.asyncio
+async def test_digest_auth_no_specified_qop():
url = "https://example.org/"
auth = DigestAuth(username="tomchristie", password="password123")
- with Client(dispatch=MockDigestAuthDispatch(qop=None)) as client:
- response = client.get(url, auth=auth)
+ client = Client(dispatch=MockDigestAuthDispatch(qop=None))
+ response = await client.get(url, auth=auth)
assert response.status_code == 200
auth = response.json()["auth"]
@pytest.mark.parametrize("qop", ("auth, auth-int", "auth,auth-int", "unknown,auth"))
-def test_digest_auth_qop_including_spaces_and_auth_returns_auth(qop: str):
+@pytest.mark.asyncio
+async def test_digest_auth_qop_including_spaces_and_auth_returns_auth(qop: str):
url = "https://example.org/"
auth = DigestAuth(username="tomchristie", password="password123")
- with Client(dispatch=MockDigestAuthDispatch(qop=qop)) as client:
- client.get(url, auth=auth)
+ client = Client(dispatch=MockDigestAuthDispatch(qop=qop))
+ await client.get(url, auth=auth)
-def test_digest_auth_qop_auth_int_not_implemented():
+@pytest.mark.asyncio
+async def test_digest_auth_qop_auth_int_not_implemented():
url = "https://example.org/"
auth = DigestAuth(username="tomchristie", password="password123")
+ client = Client(dispatch=MockDigestAuthDispatch(qop="auth-int"))
with pytest.raises(NotImplementedError):
- with Client(dispatch=MockDigestAuthDispatch(qop="auth-int")) as client:
- client.get(url, auth=auth)
+ await client.get(url, auth=auth)
-def test_digest_auth_qop_must_be_auth_or_auth_int():
+@pytest.mark.asyncio
+async def test_digest_auth_qop_must_be_auth_or_auth_int():
url = "https://example.org/"
auth = DigestAuth(username="tomchristie", password="password123")
+ client = Client(dispatch=MockDigestAuthDispatch(qop="not-auth"))
with pytest.raises(ProtocolError):
- with Client(dispatch=MockDigestAuthDispatch(qop="not-auth")) as client:
- client.get(url, auth=auth)
+ await client.get(url, auth=auth)
-def test_digest_auth_incorrect_credentials():
+@pytest.mark.asyncio
+async def test_digest_auth_incorrect_credentials():
url = "https://example.org/"
auth = DigestAuth(username="tomchristie", password="password123")
- with Client(
- dispatch=MockDigestAuthDispatch(send_response_after_attempt=2)
- ) as client:
- response = client.get(url, auth=auth)
+ client = Client(dispatch=MockDigestAuthDispatch(send_response_after_attempt=2))
+ response = await client.get(url, auth=auth)
assert response.status_code == 401
'Digest realm="httpx@example.org", qop="auth,au', # malformed fields list
],
)
-def test_digest_auth_raises_protocol_error_on_malformed_header(auth_header: str):
+@pytest.mark.asyncio
+async def test_digest_auth_raises_protocol_error_on_malformed_header(auth_header: str):
url = "https://example.org/"
auth = DigestAuth(username="tomchristie", password="password123")
+ client = Client(dispatch=MockDispatch(auth_header=auth_header, status_code=401))
with pytest.raises(ProtocolError):
- with Client(
- dispatch=MockDispatch(auth_header=auth_header, status_code=401)
- ) as client:
- client.get(url, auth=auth)
+ await client.get(url, auth=auth)
import httpx
-def test_get(server):
+@pytest.mark.asyncio
+async def test_get(server):
url = server.url
- with httpx.Client() as http:
- response = http.get(url)
+ async with httpx.Client() as http:
+ response = await http.get(url)
assert response.status_code == 200
assert response.url == url
assert response.content == b"Hello, world!"
assert response.elapsed > timedelta(0)
-def test_build_request(server):
+@pytest.mark.asyncio
+async def test_build_request(server):
url = server.url.copy_with(path="/echo_headers")
headers = {"Custom-header": "value"}
- with httpx.Client() as http:
- request = http.build_request("GET", url)
+ async with httpx.Client() as client:
+ request = client.build_request("GET", url)
request.headers.update(headers)
- response = http.send(request)
+ response = await client.send(request)
assert response.status_code == 200
assert response.url == url
assert response.json()["Custom-header"] == "value"
-def test_post(server):
- with httpx.Client() as http:
- response = http.post(server.url, data=b"Hello, world!")
+@pytest.mark.asyncio
+async def test_post(server):
+ async with httpx.Client() as client:
+ response = await client.post(server.url, data=b"Hello, world!")
assert response.status_code == 200
assert response.reason_phrase == "OK"
-def test_post_json(server):
- with httpx.Client() as http:
- response = http.post(server.url, json={"text": "Hello, world!"})
+@pytest.mark.asyncio
+async def test_post_json(server):
+ async with httpx.Client() as client:
+ response = await client.post(server.url, json={"text": "Hello, world!"})
assert response.status_code == 200
assert response.reason_phrase == "OK"
-def test_stream_response(server):
- with httpx.Client() as http:
- response = http.get(server.url, stream=True)
+@pytest.mark.asyncio
+async def test_stream_response(server):
+ async with httpx.Client() as client:
+ response = await client.get(server.url, stream=True)
assert response.status_code == 200
- content = response.read()
+ content = await response.read()
assert content == b"Hello, world!"
-def test_stream_iterator(server):
- with httpx.Client() as http:
- response = http.get(server.url, stream=True)
+@pytest.mark.asyncio
+async def test_stream_iterator(server):
+ async with httpx.Client() as client:
+ response = await client.get(server.url, stream=True)
assert response.status_code == 200
body = b""
- for chunk in response.stream():
+ async for chunk in response.stream():
body += chunk
assert body == b"Hello, world!"
-def test_raw_iterator(server):
- with httpx.Client() as http:
- response = http.get(server.url, stream=True)
+@pytest.mark.asyncio
+async def test_raw_iterator(server):
+ async with httpx.Client() as client:
+ response = await client.get(server.url, stream=True)
assert response.status_code == 200
body = b""
- for chunk in response.raw():
+ async for chunk in response.raw():
body += chunk
assert body == b"Hello, world!"
- response.close() # TODO: should Response be available as context managers?
+ await response.close()
-def test_raise_for_status(server):
- with httpx.Client() as client:
+@pytest.mark.asyncio
+async def test_raise_for_status(server):
+ async with httpx.Client() as client:
for status_code in (200, 400, 404, 500, 505):
- response = client.request(
+ response = await client.request(
"GET", server.url.copy_with(path="/status/{}".format(status_code))
)
if 400 <= status_code < 600:
assert response.raise_for_status() is None
-def test_options(server):
- with httpx.Client() as http:
- response = http.options(server.url)
+@pytest.mark.asyncio
+async def test_options(server):
+ async with httpx.Client() as client:
+ response = await client.options(server.url)
assert response.status_code == 200
assert response.reason_phrase == "OK"
-def test_head(server):
- with httpx.Client() as http:
- response = http.head(server.url)
+@pytest.mark.asyncio
+async def test_head(server):
+ async with httpx.Client() as client:
+ response = await client.head(server.url)
assert response.status_code == 200
assert response.reason_phrase == "OK"
-def test_put(server):
- with httpx.Client() as http:
- response = http.put(server.url, data=b"Hello, world!")
+@pytest.mark.asyncio
+async def test_put(server):
+ async with httpx.Client() as client:
+ response = await client.put(server.url, data=b"Hello, world!")
assert response.status_code == 200
assert response.reason_phrase == "OK"
-def test_patch(server):
- with httpx.Client() as http:
- response = http.patch(server.url, data=b"Hello, world!")
+@pytest.mark.asyncio
+async def test_patch(server):
+ async with httpx.Client() as client:
+ response = await client.patch(server.url, data=b"Hello, world!")
assert response.status_code == 200
assert response.reason_phrase == "OK"
-def test_delete(server):
- with httpx.Client() as http:
- response = http.delete(server.url)
+@pytest.mark.asyncio
+async def test_delete(server):
+ async with httpx.Client() as client:
+ response = await client.delete(server.url)
assert response.status_code == 200
assert response.reason_phrase == "OK"
-def test_base_url(server):
+@pytest.mark.asyncio
+async def test_base_url(server):
base_url = server.url
- with httpx.Client(base_url=base_url) as http:
- response = http.get("/")
+ async with httpx.Client(base_url=base_url) as client:
+ response = await client.get("/")
assert response.status_code == 200
assert response.url == base_url
-def test_uds(uds_server):
+@pytest.mark.asyncio
+async def test_uds(uds_server):
url = uds_server.url
uds = uds_server.config.uds
assert uds is not None
- with httpx.Client(uds=uds) as http:
- response = http.get(url)
+ async with httpx.Client(uds=uds) as client:
+ response = await client.get(url)
assert response.status_code == 200
assert response.text == "Hello, world!"
assert response.encoding == "iso-8859-1"
assert url.is_ssl
-class DerivedFromAsyncioBackend(httpx.AsyncioBackend):
- pass
-
-
-class AnyBackend:
- pass
-
-
-def test_client_backend_must_be_asyncio_based():
- httpx.Client(backend=httpx.AsyncioBackend())
- httpx.Client(backend=DerivedFromAsyncioBackend())
-
- with pytest.raises(ValueError):
- httpx.Client(backend=AnyBackend())
-
-
-def test_elapsed_delay(server):
- with httpx.Client() as http:
- response = http.get(server.url.copy_with(path="/slow_response/100"))
+@pytest.mark.asyncio
+async def test_elapsed_delay(server):
+ async with httpx.Client() as client:
+ response = await client.get(server.url.copy_with(path="/slow_response/100"))
assert response.elapsed.total_seconds() == pytest.approx(0.1, rel=0.2)
-def test_elapsed_delay_ignores_read_time(server):
- with httpx.Client() as http:
- response = http.get(
+@pytest.mark.asyncio
+async def test_elapsed_delay_ignores_read_time(server):
+ async with httpx.Client() as client:
+ response = await client.get(
server.url.copy_with(path="/slow_response/100"), stream=True
)
sleep(0.2)
- response.read()
+ await response.read()
assert response.elapsed.total_seconds() == pytest.approx(0.1, rel=0.2)
import json
from http.cookiejar import Cookie, CookieJar
+import pytest
+
from httpx import (
- AsyncDispatcher,
- AsyncRequest,
- AsyncResponse,
CertTypes,
Client,
Cookies,
+ Dispatcher,
+ Request,
+ Response,
TimeoutTypes,
VerifyTypes,
)
-class MockDispatch(AsyncDispatcher):
+class MockDispatch(Dispatcher):
async def send(
self,
- request: AsyncRequest,
+ request: Request,
verify: VerifyTypes = None,
cert: CertTypes = None,
timeout: TimeoutTypes = None,
- ) -> AsyncResponse:
+ ) -> Response:
if request.url.path.startswith("/echo_cookies"):
body = json.dumps({"cookies": request.headers.get("Cookie")}).encode()
- return AsyncResponse(200, content=body, request=request)
+ return Response(200, content=body, request=request)
elif request.url.path.startswith("/set_cookie"):
headers = {"set-cookie": "example-name=example-value"}
- return AsyncResponse(200, headers=headers, request=request)
+ return Response(200, headers=headers, request=request)
-def test_set_cookie():
+@pytest.mark.asyncio
+async def test_set_cookie():
"""
Send a request including a cookie.
"""
url = "http://example.org/echo_cookies"
cookies = {"example-name": "example-value"}
- with Client(dispatch=MockDispatch()) as client:
- response = client.get(url, cookies=cookies)
+ client = Client(dispatch=MockDispatch())
+ response = await client.get(url, cookies=cookies)
assert response.status_code == 200
assert response.json() == {"cookies": "example-name=example-value"}
-def test_set_cookie_with_cookiejar():
+@pytest.mark.asyncio
+async def test_set_cookie_with_cookiejar():
"""
Send a request including a cookie, using a `CookieJar` instance.
"""
)
cookies.set_cookie(cookie)
- with Client(dispatch=MockDispatch()) as client:
- response = client.get(url, cookies=cookies)
+ client = Client(dispatch=MockDispatch())
+ response = await client.get(url, cookies=cookies)
assert response.status_code == 200
assert response.json() == {"cookies": "example-name=example-value"}
-def test_setting_client_cookies_to_cookiejar():
+@pytest.mark.asyncio
+async def test_setting_client_cookies_to_cookiejar():
"""
Send a request including a cookie, using a `CookieJar` instance.
"""
)
cookies.set_cookie(cookie)
- with Client(dispatch=MockDispatch()) as client:
- client.cookies = cookies
- response = client.get(url)
+ client = Client(dispatch=MockDispatch())
+ client.cookies = cookies
+ response = await client.get(url)
assert response.status_code == 200
assert response.json() == {"cookies": "example-name=example-value"}
-def test_set_cookie_with_cookies_model():
+@pytest.mark.asyncio
+async def test_set_cookie_with_cookies_model():
"""
Send a request including a cookie, using a `Cookies` instance.
"""
cookies = Cookies()
cookies["example-name"] = "example-value"
- with Client(dispatch=MockDispatch()) as client:
- response = client.get(url, cookies=cookies)
+ client = Client(dispatch=MockDispatch())
+ response = await client.get(url, cookies=cookies)
assert response.status_code == 200
assert response.json() == {"cookies": "example-name=example-value"}
-def test_get_cookie():
+@pytest.mark.asyncio
+async def test_get_cookie():
url = "http://example.org/set_cookie"
- with Client(dispatch=MockDispatch()) as client:
- response = client.get(url)
+ client = Client(dispatch=MockDispatch())
+ response = await client.get(url)
assert response.status_code == 200
assert response.cookies["example-name"] == "example-value"
assert client.cookies["example-name"] == "example-value"
-def test_cookie_persistence():
+@pytest.mark.asyncio
+async def test_cookie_persistence():
"""
Ensure that Client instances persist cookies between requests.
"""
- with Client(dispatch=MockDispatch()) as client:
- response = client.get("http://example.org/echo_cookies")
- assert response.status_code == 200
- assert response.json() == {"cookies": None}
-
- response = client.get("http://example.org/set_cookie")
- assert response.status_code == 200
- assert response.cookies["example-name"] == "example-value"
- assert client.cookies["example-name"] == "example-value"
-
- response = client.get("http://example.org/echo_cookies")
- assert response.status_code == 200
- assert response.json() == {"cookies": "example-name=example-value"}
+ client = Client(dispatch=MockDispatch())
+
+ response = await client.get("http://example.org/echo_cookies")
+ assert response.status_code == 200
+ assert response.json() == {"cookies": None}
+
+ response = await client.get("http://example.org/set_cookie")
+ assert response.status_code == 200
+ assert response.cookies["example-name"] == "example-value"
+ assert client.cookies["example-name"] == "example-value"
+
+ response = await client.get("http://example.org/echo_cookies")
+ assert response.status_code == 200
+ assert response.json() == {"cookies": "example-name=example-value"}
import pytest
from httpx import (
- AsyncDispatcher,
- AsyncRequest,
- AsyncResponse,
CertTypes,
Client,
+ Dispatcher,
+ Request,
+ Response,
TimeoutTypes,
VerifyTypes,
__version__,
)
-class MockDispatch(AsyncDispatcher):
+class MockDispatch(Dispatcher):
async def send(
self,
- request: AsyncRequest,
+ request: Request,
verify: VerifyTypes = None,
cert: CertTypes = None,
timeout: TimeoutTypes = None,
- ) -> AsyncResponse:
+ ) -> Response:
if request.url.path.startswith("/echo_headers"):
request_headers = dict(request.headers.items())
body = json.dumps({"headers": request_headers}).encode()
- return AsyncResponse(200, content=body, request=request)
+ return Response(200, content=body, request=request)
-def test_client_header():
+@pytest.mark.asyncio
+async def test_client_header():
"""
Set a header in the Client.
"""
url = "http://example.org/echo_headers"
headers = {"Example-Header": "example-value"}
- with Client(dispatch=MockDispatch(), headers=headers) as client:
- response = client.get(url)
+ client = Client(dispatch=MockDispatch(), headers=headers)
+ response = await client.get(url)
assert response.status_code == 200
assert response.json() == {
}
-def test_header_merge():
+@pytest.mark.asyncio
+async def test_header_merge():
url = "http://example.org/echo_headers"
client_headers = {"User-Agent": "python-myclient/0.2.1"}
request_headers = {"X-Auth-Token": "FooBarBazToken"}
- with Client(dispatch=MockDispatch(), headers=client_headers) as client:
- response = client.get(url, headers=request_headers)
+ client = Client(dispatch=MockDispatch(), headers=client_headers)
+ response = await client.get(url, headers=request_headers)
assert response.status_code == 200
assert response.json() == {
}
-def test_header_merge_conflicting_headers():
+@pytest.mark.asyncio
+async def test_header_merge_conflicting_headers():
url = "http://example.org/echo_headers"
client_headers = {"X-Auth-Token": "FooBar"}
request_headers = {"X-Auth-Token": "BazToken"}
- with Client(dispatch=MockDispatch(), headers=client_headers) as client:
- response = client.get(url, headers=request_headers)
+ client = Client(dispatch=MockDispatch(), headers=client_headers)
+ response = await client.get(url, headers=request_headers)
assert response.status_code == 200
assert response.json() == {
}
-def test_header_update():
+@pytest.mark.asyncio
+async def test_header_update():
url = "http://example.org/echo_headers"
- with Client(dispatch=MockDispatch()) as client:
- first_response = client.get(url)
- client.headers.update(
- {"User-Agent": "python-myclient/0.2.1", "Another-Header": "AThing"}
- )
- second_response = client.get(url)
+ client = Client(dispatch=MockDispatch())
+ first_response = await client.get(url)
+ client.headers.update(
+ {"User-Agent": "python-myclient/0.2.1", "Another-Header": "AThing"}
+ )
+ second_response = await client.get(url)
assert first_response.status_code == 200
assert first_response.json() == {
del headers["baz"]
-def test_host_without_auth_in_header():
+@pytest.mark.asyncio
+async def test_host_without_auth_in_header():
url = "http://username:password@example.org:80/echo_headers"
- with Client(dispatch=MockDispatch()) as client:
- response = client.get(url)
+ client = Client(dispatch=MockDispatch())
+ response = await client.get(url)
assert response.status_code == 200
assert response.json() == {
def test_proxies_has_same_properties_as_dispatch():
- client = httpx.AsyncClient(
+ client = httpx.Client(
proxies="http://127.0.0.1",
verify="/path/to/verify",
cert="/path/to/cert",
],
)
def test_dispatcher_for_request(url, proxies, expected):
- client = httpx.AsyncClient(proxies=proxies)
- request = httpx.AsyncRequest("GET", url)
+ client = httpx.Client(proxies=proxies)
+ request = httpx.Request("GET", url)
dispatcher = client._dispatcher_for_request(request, client.proxies)
if expected is None:
def test_unsupported_proxy_scheme():
with pytest.raises(ValueError):
- httpx.AsyncClient(proxies="ftp://127.0.0.1")
+ httpx.Client(proxies="ftp://127.0.0.1")
import json
+import pytest
+
from httpx import (
- AsyncDispatcher,
- AsyncRequest,
- AsyncResponse,
CertTypes,
Client,
+ Dispatcher,
QueryParams,
+ Request,
+ Response,
TimeoutTypes,
VerifyTypes,
)
from httpx.models import URL
-class MockDispatch(AsyncDispatcher):
+class MockDispatch(Dispatcher):
async def send(
self,
- request: AsyncRequest,
+ request: Request,
verify: VerifyTypes = None,
cert: CertTypes = None,
timeout: TimeoutTypes = None,
- ) -> AsyncResponse:
+ ) -> Response:
if request.url.path.startswith("/echo_queryparams"):
body = json.dumps({"ok": "ok"}).encode()
- return AsyncResponse(200, content=body, request=request)
+ return Response(200, content=body, request=request)
def test_client_queryparams():
assert client.params["a"] == "b"
-def test_client_queryparams_echo():
+@pytest.mark.asyncio
+async def test_client_queryparams_echo():
url = "http://example.org/echo_queryparams"
client_queryparams = "first=str"
request_queryparams = {"second": "dict"}
- with Client(dispatch=MockDispatch(), params=client_queryparams) as client:
- response = client.get(url, params=request_queryparams)
+ client = Client(dispatch=MockDispatch(), params=client_queryparams)
+ response = await client.get(url, params=request_queryparams)
assert response.status_code == 200
assert response.url == URL(
from httpx import (
URL,
- AsyncClient,
- AsyncDispatcher,
- AsyncRequest,
- AsyncResponse,
CertTypes,
+ Client,
+ Dispatcher,
NotRedirectResponse,
RedirectBodyUnavailable,
RedirectLoop,
+ Request,
+ Response,
TimeoutTypes,
TooManyRedirects,
VerifyTypes,
)
-class MockDispatch(AsyncDispatcher):
+class MockDispatch(Dispatcher):
async def send(
self,
- request: AsyncRequest,
+ request: Request,
verify: VerifyTypes = None,
cert: CertTypes = None,
timeout: TimeoutTypes = None,
- ) -> AsyncResponse:
+ ) -> Response:
if request.url.path == "/no_redirect":
- return AsyncResponse(codes.OK, request=request)
+ return Response(codes.OK, request=request)
elif request.url.path == "/redirect_301":
status_code = codes.MOVED_PERMANENTLY
headers = {"location": "https://example.org/"}
- return AsyncResponse(status_code, headers=headers, request=request)
+ return Response(status_code, headers=headers, request=request)
elif request.url.path == "/redirect_302":
status_code = codes.FOUND
headers = {"location": "https://example.org/"}
- return AsyncResponse(status_code, headers=headers, request=request)
+ return Response(status_code, headers=headers, request=request)
elif request.url.path == "/redirect_303":
status_code = codes.SEE_OTHER
headers = {"location": "https://example.org/"}
- return AsyncResponse(status_code, headers=headers, request=request)
+ return Response(status_code, headers=headers, request=request)
elif request.url.path == "/relative_redirect":
headers = {"location": "/"}
- return AsyncResponse(codes.SEE_OTHER, headers=headers, request=request)
+ return Response(codes.SEE_OTHER, headers=headers, request=request)
elif request.url.path == "/no_scheme_redirect":
headers = {"location": "//example.org/"}
- return AsyncResponse(codes.SEE_OTHER, headers=headers, request=request)
+ return Response(codes.SEE_OTHER, headers=headers, request=request)
elif request.url.path == "/multiple_redirects":
params = parse_qs(request.url.query)
if redirect_count:
location += "?count=" + str(redirect_count)
headers = {"location": location} if count else {}
- return AsyncResponse(code, headers=headers, request=request)
+ return Response(code, headers=headers, request=request)
if request.url.path == "/redirect_loop":
headers = {"location": "/redirect_loop"}
- return AsyncResponse(codes.SEE_OTHER, headers=headers, request=request)
+ return Response(codes.SEE_OTHER, headers=headers, request=request)
elif request.url.path == "/cross_domain":
headers = {"location": "https://example.org/cross_domain_target"}
- return AsyncResponse(codes.SEE_OTHER, headers=headers, request=request)
+ return Response(codes.SEE_OTHER, headers=headers, request=request)
elif request.url.path == "/cross_domain_target":
headers = dict(request.headers.items())
content = json.dumps({"headers": headers}).encode()
- return AsyncResponse(codes.OK, content=content, request=request)
+ return Response(codes.OK, content=content, request=request)
elif request.url.path == "/redirect_body":
await request.read()
headers = {"location": "/redirect_body_target"}
- return AsyncResponse(
- codes.PERMANENT_REDIRECT, headers=headers, request=request
- )
+ return Response(codes.PERMANENT_REDIRECT, headers=headers, request=request)
elif request.url.path == "/redirect_no_body":
await request.read()
headers = {"location": "/redirect_body_target"}
- return AsyncResponse(codes.SEE_OTHER, headers=headers, request=request)
+ return Response(codes.SEE_OTHER, headers=headers, request=request)
elif request.url.path == "/redirect_body_target":
content = await request.read()
headers = dict(request.headers.items())
body = json.dumps({"body": content.decode(), "headers": headers}).encode()
- return AsyncResponse(codes.OK, content=body, request=request)
+ return Response(codes.OK, content=body, request=request)
elif request.url.path == "/cross_subdomain":
if request.headers["host"] != "www.example.org":
headers = {"location": "https://www.example.org/cross_subdomain"}
- return AsyncResponse(
+ return Response(
codes.PERMANENT_REDIRECT, headers=headers, request=request
)
else:
- return AsyncResponse(
- codes.OK, content=b"Hello, world!", request=request
- )
+ return Response(codes.OK, content=b"Hello, world!", request=request)
- return AsyncResponse(codes.OK, content=b"Hello, world!", request=request)
+ return Response(codes.OK, content=b"Hello, world!", request=request)
async def test_no_redirect(backend):
- client = AsyncClient(dispatch=MockDispatch(), backend=backend)
+ client = Client(dispatch=MockDispatch(), backend=backend)
url = "https://example.com/no_redirect"
response = await client.get(url)
assert response.status_code == 200
async def test_redirect_301(backend):
- client = AsyncClient(dispatch=MockDispatch(), backend=backend)
+ client = Client(dispatch=MockDispatch(), backend=backend)
response = await client.post("https://example.org/redirect_301")
assert response.status_code == codes.OK
assert response.url == URL("https://example.org/")
async def test_redirect_302(backend):
- client = AsyncClient(dispatch=MockDispatch(), backend=backend)
+ client = Client(dispatch=MockDispatch(), backend=backend)
response = await client.post("https://example.org/redirect_302")
assert response.status_code == codes.OK
assert response.url == URL("https://example.org/")
async def test_redirect_303(backend):
- client = AsyncClient(dispatch=MockDispatch(), backend=backend)
+ client = Client(dispatch=MockDispatch(), backend=backend)
response = await client.get("https://example.org/redirect_303")
assert response.status_code == codes.OK
assert response.url == URL("https://example.org/")
async def test_disallow_redirects(backend):
- client = AsyncClient(dispatch=MockDispatch(), backend=backend)
+ client = Client(dispatch=MockDispatch(), backend=backend)
response = await client.post(
"https://example.org/redirect_303", allow_redirects=False
)
async def test_relative_redirect(backend):
- client = AsyncClient(dispatch=MockDispatch(), backend=backend)
+ client = Client(dispatch=MockDispatch(), backend=backend)
response = await client.get("https://example.org/relative_redirect")
assert response.status_code == codes.OK
assert response.url == URL("https://example.org/")
async def test_no_scheme_redirect(backend):
- client = AsyncClient(dispatch=MockDispatch(), backend=backend)
+ client = Client(dispatch=MockDispatch(), backend=backend)
response = await client.get("https://example.org/no_scheme_redirect")
assert response.status_code == codes.OK
assert response.url == URL("https://example.org/")
async def test_fragment_redirect(backend):
- client = AsyncClient(dispatch=MockDispatch(), backend=backend)
+ client = Client(dispatch=MockDispatch(), backend=backend)
response = await client.get("https://example.org/relative_redirect#fragment")
assert response.status_code == codes.OK
assert response.url == URL("https://example.org/#fragment")
async def test_multiple_redirects(backend):
- client = AsyncClient(dispatch=MockDispatch(), backend=backend)
+ client = Client(dispatch=MockDispatch(), backend=backend)
response = await client.get("https://example.org/multiple_redirects?count=20")
assert response.status_code == codes.OK
assert response.url == URL("https://example.org/multiple_redirects")
async def test_too_many_redirects(backend):
- client = AsyncClient(dispatch=MockDispatch(), backend=backend)
+ client = Client(dispatch=MockDispatch(), backend=backend)
with pytest.raises(TooManyRedirects):
await client.get("https://example.org/multiple_redirects?count=21")
async def test_too_many_redirects_calling_next(backend):
- client = AsyncClient(dispatch=MockDispatch(), backend=backend)
+ client = Client(dispatch=MockDispatch(), backend=backend)
url = "https://example.org/multiple_redirects?count=21"
response = await client.get(url, allow_redirects=False)
with pytest.raises(TooManyRedirects):
async def test_redirect_loop(backend):
- client = AsyncClient(dispatch=MockDispatch(), backend=backend)
+ client = Client(dispatch=MockDispatch(), backend=backend)
with pytest.raises(RedirectLoop):
await client.get("https://example.org/redirect_loop")
async def test_redirect_loop_calling_next(backend):
- client = AsyncClient(dispatch=MockDispatch(), backend=backend)
+ client = Client(dispatch=MockDispatch(), backend=backend)
url = "https://example.org/redirect_loop"
response = await client.get(url, allow_redirects=False)
with pytest.raises(RedirectLoop):
async def test_cross_domain_redirect(backend):
- client = AsyncClient(dispatch=MockDispatch(), backend=backend)
+ client = Client(dispatch=MockDispatch(), backend=backend)
url = "https://example.com/cross_domain"
headers = {"Authorization": "abc"}
response = await client.get(url, headers=headers)
async def test_same_domain_redirect(backend):
- client = AsyncClient(dispatch=MockDispatch(), backend=backend)
+ client = Client(dispatch=MockDispatch(), backend=backend)
url = "https://example.org/cross_domain"
headers = {"Authorization": "abc"}
response = await client.get(url, headers=headers)
"""
A 308 redirect should preserve the request body.
"""
- client = AsyncClient(dispatch=MockDispatch(), backend=backend)
+ client = Client(dispatch=MockDispatch(), backend=backend)
url = "https://example.org/redirect_body"
data = b"Example request body"
response = await client.post(url, data=data)
"""
A 303 redirect should remove the request body.
"""
- client = AsyncClient(dispatch=MockDispatch(), backend=backend)
+ client = Client(dispatch=MockDispatch(), backend=backend)
url = "https://example.org/redirect_no_body"
data = b"Example request body"
response = await client.post(url, data=data)
async def test_cannot_redirect_streaming_body(backend):
- client = AsyncClient(dispatch=MockDispatch(), backend=backend)
+ client = Client(dispatch=MockDispatch(), backend=backend)
url = "https://example.org/redirect_body"
async def streaming_body():
async def test_cross_subdomain_redirect(backend):
- client = AsyncClient(dispatch=MockDispatch(), backend=backend)
+ client = Client(dispatch=MockDispatch(), backend=backend)
url = "https://example.com/cross_subdomain"
response = await client.get(url)
assert response.url == URL("https://www.example.org/cross_subdomain")
-class MockCookieDispatch(AsyncDispatcher):
+class MockCookieDispatch(Dispatcher):
async def send(
self,
- request: AsyncRequest,
+ request: Request,
verify: VerifyTypes = None,
cert: CertTypes = None,
timeout: TimeoutTypes = None,
- ) -> AsyncResponse:
+ ) -> Response:
if request.url.path == "/":
if "cookie" in request.headers:
content = b"Logged in"
else:
content = b"Not logged in"
- return AsyncResponse(codes.OK, content=content, request=request)
+ return Response(codes.OK, content=content, request=request)
elif request.url.path == "/login":
status_code = codes.SEE_OTHER
),
}
- return AsyncResponse(status_code, headers=headers, request=request)
+ return Response(status_code, headers=headers, request=request)
elif request.url.path == "/logout":
status_code = codes.SEE_OTHER
"httponly; samesite=lax"
),
}
- return AsyncResponse(status_code, headers=headers, request=request)
+ return Response(status_code, headers=headers, request=request)
async def test_redirect_cookie_behavior(backend):
- client = AsyncClient(dispatch=MockCookieDispatch(), backend=backend)
+ client = Client(dispatch=MockCookieDispatch(), backend=backend)
# The client is not logged in.
response = await client.get("https://example.com/")
async def redirect_301(scope, receive, send):
await send(
- {
- "type": "http.response.start",
- "status": 301,
- "headers": [[b"location", b"/"]],
- }
+ {"type": "http.response.start", "status": 301, "headers": [[b"location", b"/"]]}
)
await send({"type": "http.response.body"})
import pytest
from h2.settings import SettingCodes
-from httpx import AsyncClient, Client, Response, Timeout
+from httpx import Client, Response, Timeout
from .utils import MockHTTP2Backend
return Response(200, headers=headers, content=content)
-def test_http2_get_request():
+@pytest.mark.asyncio
+async def test_http2_get_request():
backend = MockHTTP2Backend(app=app)
- with Client(backend=backend) as client:
- response = client.get("http://example.org")
-
- assert response.status_code == 200
- assert json.loads(response.content) == {"method": "GET", "path": "/", "body": ""}
-
-
-async def test_async_http2_get_request(backend):
- backend = MockHTTP2Backend(app=app, backend=backend)
-
- async with AsyncClient(backend=backend) as client:
+ async with Client(backend=backend) as client:
response = await client.get("http://example.org")
assert response.status_code == 200
assert json.loads(response.content) == {"method": "GET", "path": "/", "body": ""}
-def test_http2_post_request():
+@pytest.mark.asyncio
+async def test_http2_post_request():
backend = MockHTTP2Backend(app=app)
- with Client(backend=backend) as client:
- response = client.post("http://example.org", data=b"<data>")
-
- assert response.status_code == 200
- assert json.loads(response.content) == {
- "method": "POST",
- "path": "/",
- "body": "<data>",
- }
-
-
-async def test_async_http2_post_request(backend):
- backend = MockHTTP2Backend(app=app, backend=backend)
-
- async with AsyncClient(backend=backend) as client:
+ async with Client(backend=backend) as client:
response = await client.post("http://example.org", data=b"<data>")
assert response.status_code == 200
}
-def test_http2_large_post_request():
+@pytest.mark.asyncio
+async def test_http2_large_post_request():
backend = MockHTTP2Backend(app=app)
data = b"a" * 100000
- with Client(backend=backend) as client:
- response = client.post("http://example.org", data=data)
- assert response.status_code == 200
- assert json.loads(response.content) == {
- "method": "POST",
- "path": "/",
- "body": data.decode(),
- }
-
-
-async def test_async_http2_large_post_request(backend):
- backend = MockHTTP2Backend(app=app, backend=backend)
-
- data = b"a" * 100000
- async with AsyncClient(backend=backend) as client:
+ async with Client(backend=backend) as client:
response = await client.post("http://example.org", data=data)
assert response.status_code == 200
assert json.loads(response.content) == {
}
-def test_http2_multiple_requests():
+@pytest.mark.asyncio
+async def test_http2_multiple_requests():
backend = MockHTTP2Backend(app=app)
- with Client(backend=backend) as client:
- response_1 = client.get("http://example.org/1")
- response_2 = client.get("http://example.org/2")
- response_3 = client.get("http://example.org/3")
-
- assert response_1.status_code == 200
- assert json.loads(response_1.content) == {"method": "GET", "path": "/1", "body": ""}
-
- assert response_2.status_code == 200
- assert json.loads(response_2.content) == {"method": "GET", "path": "/2", "body": ""}
-
- assert response_3.status_code == 200
- assert json.loads(response_3.content) == {"method": "GET", "path": "/3", "body": ""}
-
-
-async def test_async_http2_multiple_requests(backend):
- backend = MockHTTP2Backend(app=app, backend=backend)
-
- async with AsyncClient(backend=backend) as client:
+ async with Client(backend=backend) as client:
response_1 = await client.get("http://example.org/1")
response_2 = await client.get("http://example.org/2")
response_3 = await client.get("http://example.org/3")
assert json.loads(response_3.content) == {"method": "GET", "path": "/3", "body": ""}
-def test_http2_reconnect():
+@pytest.mark.asyncio
+async def test_http2_reconnect():
"""
If a connection has been dropped between requests, then we should
be seemlessly reconnected.
"""
backend = MockHTTP2Backend(app=app)
- with Client(backend=backend) as client:
- response_1 = client.get("http://example.org/1")
- backend.server.close_connection = True
- response_2 = client.get("http://example.org/2")
-
- assert response_1.status_code == 200
- assert json.loads(response_1.content) == {"method": "GET", "path": "/1", "body": ""}
-
- assert response_2.status_code == 200
- assert json.loads(response_2.content) == {"method": "GET", "path": "/2", "body": ""}
-
-
-async def test_async_http2_reconnect(backend):
- """
- If a connection has been dropped between requests, then we should
- be seemlessly reconnected.
- """
- backend = MockHTTP2Backend(app=app, backend=backend)
-
- async with AsyncClient(backend=backend) as client:
+ async with Client(backend=backend) as client:
response_1 = await client.get("http://example.org/1")
backend.server.close_connection = True
response_2 = await client.get("http://example.org/2")
async def test_http2_settings_in_handshake(backend):
backend = MockHTTP2Backend(app=app, backend=backend)
- async with AsyncClient(backend=backend) as client:
+ async with Client(backend=backend) as client:
await client.get("http://example.org")
h2_conn = backend.server.conn
async def test_http2_live_request(backend):
- async with AsyncClient(backend=backend) as client:
+ async with Client(backend=backend) as client:
try:
resp = await client.get("https://nghttp2.org/httpbin/anything")
except Timeout:
+++ /dev/null
-import json
-
-from httpx import (
- AsyncClient,
- CertTypes,
- Client,
- Dispatcher,
- Request,
- Response,
- TimeoutTypes,
- VerifyTypes,
-)
-
-
-def streaming_body():
- for part in [b"Hello", b", ", b"world!"]:
- yield part
-
-
-class MockDispatch(Dispatcher):
- def send(
- self,
- request: Request,
- verify: VerifyTypes = None,
- cert: CertTypes = None,
- timeout: TimeoutTypes = None,
- ) -> Response:
- if request.url.path == "/streaming_response":
- return Response(200, content=streaming_body(), request=request)
- elif request.url.path == "/echo_request_body":
- content = request.read()
- return Response(200, content=content, request=request)
- elif request.url.path == "/echo_request_body_streaming":
- content = b"".join([part for part in request.stream()])
- return Response(200, content=content, request=request)
- else:
- body = json.dumps({"hello": "world"}).encode()
- return Response(200, content=body, request=request)
-
-
-def test_threaded_dispatch():
- """
- Use a synchronous 'Dispatcher' class with the client.
- Calls to the dispatcher will end up running within a thread pool.
- """
- url = "https://example.org/"
- with Client(dispatch=MockDispatch()) as client:
- response = client.get(url)
-
- assert response.status_code == 200
- assert response.json() == {"hello": "world"}
-
-
-async def test_async_threaded_dispatch(backend):
- """
- Use a synchronous 'Dispatcher' class with the async client.
- Calls to the dispatcher will end up running within a thread pool.
- """
- url = "https://example.org/"
- async with AsyncClient(dispatch=MockDispatch(), backend=backend) as client:
- response = await client.get(url)
-
- assert response.status_code == 200
- assert response.json() == {"hello": "world"}
-
-
-def test_threaded_streaming_response():
- url = "https://example.org/streaming_response"
- with Client(dispatch=MockDispatch()) as client:
- response = client.get(url)
-
- assert response.status_code == 200
- assert response.text == "Hello, world!"
-
-
-def test_threaded_streaming_request():
- url = "https://example.org/echo_request_body"
- with Client(dispatch=MockDispatch()) as client:
- response = client.post(url, data=streaming_body())
-
- assert response.status_code == 200
- assert response.text == "Hello, world!"
-
-
-def test_threaded_request_body():
- url = "https://example.org/echo_request_body"
- with Client(dispatch=MockDispatch()) as client:
- response = client.post(url, data=b"Hello, world!")
-
- assert response.status_code == 200
- assert response.text == "Hello, world!"
-
-
-def test_threaded_request_body_streaming():
- url = "https://example.org/echo_request_body_streaming"
- with Client(dispatch=MockDispatch()) as client:
- response = client.post(url, data=b"Hello, world!")
-
- assert response.status_code == 200
- assert response.text == "Hello, world!"
-
-
-def test_dispatch_class():
- """
- Use a synchronous 'Dispatcher' class directly.
- """
- url = "https://example.org/"
- with MockDispatch() as dispatcher:
- response = dispatcher.request("GET", url)
-
- assert response.status_code == 200
- assert response.json() == {"hello": "world"}
def test_url_encoded_data():
- for RequestClass in (httpx.Request, httpx.AsyncRequest):
- request = RequestClass("POST", "http://example.org", data={"test": "123"})
- assert request.headers["Content-Type"] == "application/x-www-form-urlencoded"
- assert request.content == b"test=123"
+ request = httpx.Request("POST", "http://example.org", data={"test": "123"})
+ assert request.headers["Content-Type"] == "application/x-www-form-urlencoded"
+ assert request.content == b"test=123"
def test_json_encoded_data():
- for RequestClass in (httpx.Request, httpx.AsyncRequest):
- request = RequestClass("POST", "http://example.org", json={"test": 123})
- assert request.headers["Content-Type"] == "application/json"
- assert request.content == b'{"test": 123}'
+ request = httpx.Request("POST", "http://example.org", json={"test": 123})
+ assert request.headers["Content-Type"] == "application/json"
+ assert request.content == b'{"test": 123}'
def test_transfer_encoding_header():
- def streaming_body(data):
+ async def streaming_body(data):
yield data # pragma: nocover
data = streaming_body(b"test 123")
def test_override_content_length_header():
- def streaming_body(data):
+ async def streaming_body(data):
yield data # pragma: nocover
data = streaming_body(b"test 123")
assert response.encoding == "iso-8859-1"
-def test_read_response():
+@pytest.mark.asyncio
+async def test_read_response():
response = httpx.Response(200, content=b"Hello, world!")
assert response.status_code == 200
assert response.encoding == "ascii"
assert response.is_closed
- content = response.read()
+ content = await response.read()
assert content == b"Hello, world!"
assert response.content == b"Hello, world!"
assert response.is_closed
-def test_raw_interface():
+@pytest.mark.asyncio
+async def test_raw_interface():
response = httpx.Response(200, content=b"Hello, world!")
raw = b""
- for part in response.raw():
+ async for part in response.raw():
raw += part
assert raw == b"Hello, world!"
-def test_stream_interface():
- response = httpx.Response(200, content=b"Hello, world!")
-
- content = b""
- for part in response.stream():
- content += part
- assert content == b"Hello, world!"
-
-
@pytest.mark.asyncio
-async def test_async_stream_interface():
- response = httpx.AsyncResponse(200, content=b"Hello, world!")
+async def test_stream_interface():
+ response = httpx.Response(200, content=b"Hello, world!")
content = b""
async for part in response.stream():
assert content == b"Hello, world!"
-def test_stream_interface_after_read():
+@pytest.mark.asyncio
+async def test_stream_text():
response = httpx.Response(200, content=b"Hello, world!")
- response.read()
+ await response.read()
- content = b""
- for part in response.stream():
+ content = ""
+ async for part in response.stream_text():
content += part
- assert content == b"Hello, world!"
+ assert content == "Hello, world!"
@pytest.mark.asyncio
-async def test_async_stream_interface_after_read():
- response = httpx.AsyncResponse(200, content=b"Hello, world!")
+async def test_stream_interface_after_read():
+ response = httpx.Response(200, content=b"Hello, world!")
await response.read()
assert content == b"Hello, world!"
-def test_streaming_response():
- response = httpx.Response(200, content=streaming_body())
-
- assert response.status_code == 200
- assert not response.is_closed
-
- content = response.read()
-
- assert content == b"Hello, world!"
- assert response.content == b"Hello, world!"
- assert response.is_closed
-
-
@pytest.mark.asyncio
-async def test_async_streaming_response():
- response = httpx.AsyncResponse(200, content=async_streaming_body())
+async def test_streaming_response():
+ response = httpx.Response(200, content=async_streaming_body())
assert response.status_code == 200
assert not response.is_closed
assert response.is_closed
-def test_cannot_read_after_stream_consumed():
- response = httpx.Response(200, content=streaming_body())
-
- content = b""
- for part in response.stream():
- content += part
-
- with pytest.raises(httpx.StreamConsumed):
- response.read()
-
-
@pytest.mark.asyncio
-async def test_async_cannot_read_after_stream_consumed():
- response = httpx.AsyncResponse(200, content=async_streaming_body())
+async def test_cannot_read_after_stream_consumed():
+ response = httpx.Response(200, content=async_streaming_body())
content = b""
async for part in response.stream():
await response.read()
-def test_cannot_read_after_response_closed():
- response = httpx.Response(200, content=streaming_body())
-
- response.close()
-
- with pytest.raises(httpx.ResponseClosed):
- response.read()
-
-
@pytest.mark.asyncio
-async def test_async_cannot_read_after_response_closed():
- response = httpx.AsyncResponse(200, content=async_streaming_body())
+async def test_cannot_read_after_response_closed():
+ response = httpx.Response(200, content=async_streaming_body())
await response.close()
def test_link_headers(headers, expected):
response = httpx.Response(200, content=None, headers=headers)
assert response.links == expected
-
-
-@pytest.mark.asyncio
-async def test_stream_text():
- async def iterator():
- yield b"Hello, world!"
-
- response = httpx.AsyncResponse(200, content=iterator().__aiter__())
-
- await response.read()
-
- content = ""
- async for part in response.stream_text():
- content += part
- assert content == "Hello, world!"
+++ /dev/null
-import sys
-
-import pytest
-
-import httpx
-
-
-def pytest_configure(config):
- # Allow to 'import requests' without having to write 'import httpx as requests'.
- # This means we *could* literally copy-paste Requests test code as-is, if relevant.
- sys.modules["requests"] = httpx
-
-
-@pytest.fixture(autouse=True)
-def patch_httpx(monkeypatch):
- """Monkey-patch Requests APIs onto HTTPX."""
- monkeypatch.setattr(httpx, "session", httpx.Client, raising=False)
+++ /dev/null
-"""Test compatibility with the Requests high-level API."""
-import pytest
-import requests
-
-
-@pytest.mark.copied_from(
- "https://github.com/psf/requests/blob/v2.22.0/tests/test_requests.py#L61-L70"
-)
-def test_entrypoints():
- requests.session
- requests.session().get
- requests.session().head
- requests.get
- requests.head
- requests.put
- requests.patch
- requests.post
-
-
-@pytest.mark.copied_from(
- "https://github.com/psf/requests/blob/v2.22.0/tests/test_requests.py#L72",
- changes=["added noqa comment to silence flake8"],
-)
-@pytest.mark.xfail(
- reason="PoolManager has no obvious equivalent in HTTPX", raises=ImportError
-)
-def test_poolmanager_entrypoint():
- # Not really an entry point, but people rely on it.
- from requests.packages.urllib3.poolmanager import PoolManager # noqa: F401
import httpx
-def test_get(server):
- response = httpx.get(server.url)
+@pytest.mark.asyncio
+async def test_get(server):
+ response = await httpx.get(server.url)
assert response.status_code == 200
assert response.reason_phrase == "OK"
assert response.text == "Hello, world!"
assert response.http_version == "HTTP/1.1"
-def test_post(server):
- response = httpx.post(server.url, data=b"Hello, world!")
+@pytest.mark.asyncio
+async def test_post(server):
+ response = await httpx.post(server.url, data=b"Hello, world!")
assert response.status_code == 200
assert response.reason_phrase == "OK"
-def test_post_byte_iterator(server):
- def data():
+@pytest.mark.asyncio
+async def test_post_byte_iterator(server):
+ async def data():
yield b"Hello"
yield b", "
yield b"world!"
- response = httpx.post(server.url, data=data())
+ response = await httpx.post(server.url, data=data())
assert response.status_code == 200
assert response.reason_phrase == "OK"
-def test_options(server):
- response = httpx.options(server.url)
+@pytest.mark.asyncio
+async def test_options(server):
+ response = await httpx.options(server.url)
assert response.status_code == 200
assert response.reason_phrase == "OK"
-def test_head(server):
- response = httpx.head(server.url)
+@pytest.mark.asyncio
+async def test_head(server):
+ response = await httpx.head(server.url)
assert response.status_code == 200
assert response.reason_phrase == "OK"
-def test_put(server):
- response = httpx.put(server.url, data=b"Hello, world!")
+@pytest.mark.asyncio
+async def test_put(server):
+ response = await httpx.put(server.url, data=b"Hello, world!")
assert response.status_code == 200
assert response.reason_phrase == "OK"
-def test_patch(server):
- response = httpx.patch(server.url, data=b"Hello, world!")
+@pytest.mark.asyncio
+async def test_patch(server):
+ response = await httpx.patch(server.url, data=b"Hello, world!")
assert response.status_code == 200
assert response.reason_phrase == "OK"
-def test_delete(server):
- response = httpx.delete(server.url)
+@pytest.mark.asyncio
+async def test_delete(server):
+ response = await httpx.delete(server.url)
assert response.status_code == 200
assert response.reason_phrase == "OK"
-def test_get_invalid_url(server):
+@pytest.mark.asyncio
+async def test_get_invalid_url(server):
with pytest.raises(httpx.InvalidURL):
- httpx.get("invalid://example.org")
+ await httpx.get("invalid://example.org")
raise ValueError()
-def test_asgi():
+@pytest.mark.asyncio
+async def test_asgi():
client = httpx.Client(app=hello_world)
- response = client.get("http://www.example.org/")
- assert response.status_code == 200
- assert response.text == "Hello, World!"
-
-
-async def test_asgi_async(backend):
- client = httpx.AsyncClient(app=hello_world, backend=backend)
response = await client.get("http://www.example.org/")
assert response.status_code == 200
assert response.text == "Hello, World!"
-def test_asgi_upload():
+@pytest.mark.asyncio
+async def test_asgi_upload():
client = httpx.Client(app=echo_body)
- response = client.post("http://www.example.org/", data=b"example")
- assert response.status_code == 200
- assert response.text == "example"
-
-
-async def test_asgi_upload_async(backend):
- client = httpx.AsyncClient(app=echo_body, backend=backend)
response = await client.post("http://www.example.org/", data=b"example")
assert response.status_code == 200
assert response.text == "example"
-def test_asgi_exc():
+@pytest.mark.asyncio
+async def test_asgi_exc():
client = httpx.Client(app=raise_exc)
- with pytest.raises(ValueError):
- client.get("http://www.example.org/")
-
-
-async def test_asgi_exc_async(backend):
- client = httpx.AsyncClient(app=raise_exc, backend=backend)
with pytest.raises(ValueError):
await client.get("http://www.example.org/")
-def test_asgi_exc_after_response():
+@pytest.mark.asyncio
+async def test_asgi_exc_after_response():
client = httpx.Client(app=raise_exc_after_response)
- with pytest.raises(ValueError):
- client.get("http://www.example.org/")
-
-
-async def test_asgi_exc_after_response_async(backend):
- client = httpx.AsyncClient(app=raise_exc_after_response, backend=backend)
with pytest.raises(ValueError):
await client.get("http://www.example.org/")
assert response.content == body
-def test_streaming():
+@pytest.mark.asyncio
+async def test_streaming():
body = b"test 123"
compressor = zlib.compressobj(9, zlib.DEFLATED, zlib.MAX_WBITS | 16)
- def compress(body):
+ async def compress(body):
yield compressor.compress(body)
yield compressor.flush()
headers = [(b"Content-Encoding", b"gzip")]
response = httpx.Response(200, headers=headers, content=compress(body))
assert not hasattr(response, "body")
- assert response.read() == body
+ assert await response.read() == body
@pytest.mark.parametrize("header_value", (b"deflate", b"gzip", b"br", b"identity"))
),
],
)
-def test_text_decoder(data, encoding):
- def iterator():
+@pytest.mark.asyncio
+async def test_text_decoder(data, encoding):
+ async def iterator():
nonlocal data
for chunk in data:
yield chunk
response = httpx.Response(200, content=iterator())
- assert "".join(response.stream_text()) == (b"".join(data)).decode(encoding)
+ await response.read()
+ assert response.text == (b"".join(data)).decode(encoding)
-def test_text_decoder_known_encoding():
- def iterator():
+@pytest.mark.asyncio
+async def test_text_decoder_known_encoding():
+ async def iterator():
yield b"\x83g"
yield b"\x83"
yield b"\x89\x83x\x83\x8b"
content=iterator(),
)
- assert "".join(response.stream_text()) == "トラベル"
+ await response.read()
+ assert "".join(response.text) == "トラベル"
def test_text_decoder_empty_cases():
body = b"test 123"
response = httpx.Response(200, headers=headers, content=body)
-
- assert response.read() == body
+ assert response.content == body
class MockDispatch(Dispatcher):
- def send(
+ async def send(
self,
request: Request,
verify: VerifyTypes = None,
timeout: TimeoutTypes = None,
http_versions: HTTPVersionTypes = None,
) -> Response:
- return Response(200, content=request.read())
+ content = await request.read()
+ return Response(200, content=content)
@pytest.mark.parametrize(("value,output"), (("abc", b"abc"), (b"abc", b"abc")))
-def test_multipart(value, output):
+@pytest.mark.asyncio
+async def test_multipart(value, output):
client = Client(dispatch=MockDispatch())
# Test with a single-value 'data' argument, and a plain file 'files' argument.
data = {"text": value}
files = {"file": io.BytesIO(b"<file content>")}
- response = client.post("http://127.0.0.1:8000/", data=data, files=files)
+ response = await client.post("http://127.0.0.1:8000/", data=data, files=files)
assert response.status_code == 200
# We're using the cgi module to verify the behavior here, which is a
@pytest.mark.parametrize(("key"), (b"abc", 1, 2.3, None))
-def test_multipart_invalid_key(key):
+@pytest.mark.asyncio
+async def test_multipart_invalid_key(key):
client = Client(dispatch=MockDispatch())
data = {key: "abc"}
files = {"file": io.BytesIO(b"<file content>")}
with pytest.raises(TypeError) as e:
- client.post("http://127.0.0.1:8000/", data=data, files=files)
+ await client.post("http://127.0.0.1:8000/", data=data, files=files)
assert "Invalid type for name" in str(e.value)
@pytest.mark.parametrize(("value"), (1, 2.3, None, [None, "abc"], {None: "abc"}))
-def test_multipart_invalid_value(value):
+@pytest.mark.asyncio
+async def test_multipart_invalid_value(value):
client = Client(dispatch=MockDispatch())
data = {"text": value}
files = {"file": io.BytesIO(b"<file content>")}
with pytest.raises(TypeError) as e:
- client.post("http://127.0.0.1:8000/", data=data, files=files)
+ await client.post("http://127.0.0.1:8000/", data=data, files=files)
assert "Invalid type for value" in str(e.value)
-def test_multipart_file_tuple():
+@pytest.mark.asyncio
+async def test_multipart_file_tuple():
client = Client(dispatch=MockDispatch())
# Test with a list of values 'data' argument, and a tuple style 'files' argument.
data = {"text": ["abc"]}
files = {"file": ("name.txt", io.BytesIO(b"<file content>"))}
- response = client.post("http://127.0.0.1:8000/", data=data, files=files)
+ response = await client.post("http://127.0.0.1:8000/", data=data, files=files)
assert response.status_code == 200
# We're using the cgi module to verify the behavior here, which is a
import pytest
from httpx import (
- AsyncClient,
Client,
ConnectTimeout,
PoolLimits,
async def test_read_timeout(server, backend):
timeout = TimeoutConfig(read_timeout=1e-6)
- async with AsyncClient(timeout=timeout, backend=backend) as client:
+ async with Client(timeout=timeout, backend=backend) as client:
with pytest.raises(ReadTimeout):
await client.get(server.url.copy_with(path="/slow_response"))
async def test_write_timeout(server, backend):
timeout = TimeoutConfig(write_timeout=1e-6)
- async with AsyncClient(timeout=timeout, backend=backend) as client:
+ async with Client(timeout=timeout, backend=backend) as client:
with pytest.raises(WriteTimeout):
data = b"*" * 1024 * 1024 * 100
await client.put(server.url.copy_with(path="/slow_response"), data=data)
async def test_connect_timeout(server, backend):
timeout = TimeoutConfig(connect_timeout=1e-6)
- async with AsyncClient(timeout=timeout, backend=backend) as client:
+ async with Client(timeout=timeout, backend=backend) as client:
with pytest.raises(ConnectTimeout):
# See https://stackoverflow.com/questions/100841/
await client.get("http://10.255.255.1/")
async def test_pool_timeout(server, backend):
pool_limits = PoolLimits(hard_limit=1, pool_timeout=1e-4)
- async with AsyncClient(pool_limits=pool_limits, backend=backend) as client:
+ async with Client(pool_limits=pool_limits, backend=backend) as client:
response = await client.get(server.url, stream=True)
with pytest.raises(PoolTimeout):
await client.get("http://localhost:8000/")
await response.read()
-
-
-def test_sync_infinite_timeout(server):
- """Regression test for a bug that occurred under Python 3.6.
-
- See: https://github.com/encode/httpx/issues/382
- """
- no_timeout = TimeoutConfig()
- with Client(timeout=no_timeout) as client:
- client.get(server.url.copy_with(path="/slow_response/50"))
@pytest.mark.asyncio
async def test_logs_debug(server, capsys):
with override_log_level("debug"):
- async with httpx.AsyncClient() as client:
+ async with httpx.Client() as client:
response = await client.get(server.url)
assert response.status_code == 200
stderr = capsys.readouterr().err
@pytest.mark.asyncio
async def test_logs_trace(server, capsys):
with override_log_level("trace"):
- async with httpx.AsyncClient() as client:
+ async with httpx.Client() as client:
response = await client.get(server.url)
assert response.status_code == 200
stderr = capsys.readouterr().err
@pytest.mark.asyncio
async def test_logs_redirect_chain(server, capsys):
with override_log_level("debug"):
- async with httpx.AsyncClient() as client:
+ async with httpx.Client() as client:
response = await client.get(server.url.copy_with(path="/redirect_301"))
assert response.status_code == 200
+++ /dev/null
-import sys
-
-import pytest
-
-import httpx
-
-
-def hello_world(environ, start_response):
- status = "200 OK"
- output = b"Hello, World!"
-
- response_headers = [
- ("Content-type", "text/plain"),
- ("Content-Length", str(len(output))),
- ]
-
- start_response(status, response_headers)
-
- return [output]
-
-
-def echo_body(environ, start_response):
- status = "200 OK"
- output = environ["wsgi.input"].read()
-
- response_headers = [
- ("Content-type", "text/plain"),
- ("Content-Length", str(len(output))),
- ]
-
- start_response(status, response_headers)
-
- return [output]
-
-
-def echo_body_with_response_stream(environ, start_response):
- status = "200 OK"
-
- response_headers = [("Content-Type", "text/plain")]
-
- start_response(status, response_headers)
-
- def output_generator(f):
- while True:
- output = f.read(2)
- if not output:
- break
- yield output
-
- return output_generator(f=environ["wsgi.input"])
-
-
-def raise_exc(environ, start_response):
- status = "500 Server Error"
- output = b"Nope!"
-
- response_headers = [
- ("Content-type", "text/plain"),
- ("Content-Length", str(len(output))),
- ]
-
- try:
- raise ValueError()
- except ValueError:
- exc_info = sys.exc_info()
- start_response(status, response_headers, exc_info=exc_info)
-
- return [output]
-
-
-def test_wsgi():
- client = httpx.Client(app=hello_world)
- response = client.get("http://www.example.org/")
- assert response.status_code == 200
- assert response.text == "Hello, World!"
-
-
-def test_wsgi_upload():
- client = httpx.Client(app=echo_body)
- response = client.post("http://www.example.org/", data=b"example")
- assert response.status_code == 200
- assert response.text == "example"
-
-
-def test_wsgi_upload_with_response_stream():
- client = httpx.Client(app=echo_body_with_response_stream)
- response = client.post("http://www.example.org/", data=b"example")
- assert response.status_code == 200
- assert response.text == "example"
-
-
-def test_wsgi_exc():
- client = httpx.Client(app=raise_exc)
- with pytest.raises(ValueError):
- client.get("http://www.example.org/")