pax_global_header 0000666 0000000 0000000 00000000064 15265474634 0014532 g ustar 00root root 0000000 0000000 52 comment=6dd59b947a1eaab33040805eda3d7a5b18b5b7d8
pydantic-httpx2-6dd59b9/ 0000775 0000000 0000000 00000000000 15265474634 0015224 5 ustar 00root root 0000000 0000000 pydantic-httpx2-6dd59b9/.github/ 0000775 0000000 0000000 00000000000 15265474634 0016564 5 ustar 00root root 0000000 0000000 pydantic-httpx2-6dd59b9/.github/CONTRIBUTING.md 0000664 0000000 0000000 00000020040 15265474634 0021011 0 ustar 00root root 0000000 0000000 # Contributing
Thank you for being interested in contributing to HTTPX2.
There are many ways you can contribute to the project:
- Try HTTPX2 and [report bugs/issues you find](https://github.com/pydantic/httpx2/issues/new)
- [Implement new features](https://github.com/pydantic/httpx2/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
- [Review Pull Requests of others](https://github.com/pydantic/httpx2/pulls)
- Write documentation
- Participate in discussions
## Reporting Bugs or Other Issues
Found something that HTTPX2 should support?
Stumbled upon some unexpected behaviour?
Contributions should generally start out with [a discussion](https://github.com/pydantic/httpx2/discussions).
Possible bugs and feature requests may be raised in the "Q&A" category. We can then
determine if the discussion needs to be escalated into an "Issue" or not, or if we'd
consider a pull request.
Try to be more descriptive as you can and in case of a bug report,
provide as much information as possible like:
- OS platform
- Python version
- Installed dependencies and versions (`python -m pip freeze`)
- Code snippet
- Error traceback
You should always try to reduce any examples to the *simplest possible case*
that demonstrates the issue.
Some possibly useful tips for narrowing down potential issues...
- Does the issue exist on HTTP/1.1, or HTTP/2, or both?
- Does the issue exist with `Client`, `AsyncClient`, or both?
- When using `AsyncClient` does the issue exist when using `asyncio` or `trio`, or both?
## Development
To start developing HTTPX2 create a **fork** of the
[HTTPX2 repository](https://github.com/pydantic/httpx2) on GitHub.
Then clone your fork with the following command replacing `YOUR-USERNAME` with
your GitHub username:
```shell
$ git clone https://github.com/YOUR-USERNAME/httpx2
```
You can now install the project and its dependencies using:
```shell
$ cd httpx2
$ scripts/install
```
## Testing and Linting
We use custom shell scripts to automate testing, linting,
and documentation building workflow.
To run the tests, use:
```shell
$ scripts/test
```
You can run a single test script like this:
```shell
$ scripts/test -- tests/test_multipart.py
```
To run the code auto-formatting:
```shell
$ scripts/lint
```
Lastly, to run code checks separately (they are also run as part of `scripts/test`), run:
```shell
$ scripts/check
```
## Documenting
Documentation pages are located under the `docs/` folder.
To run the documentation site locally (useful for previewing changes), use:
```shell
$ scripts/docs
```
## Resolving Build / CI Failures
Once you've submitted your pull request, the test suite will automatically run, and the results will show up in GitHub.
If the test suite fails, you'll want to click through to the "Details" link, and try to identify why the test suite failed.
Here are some common ways the test suite can fail:
### Check Job Failed
This job failing means there is either a code formatting issue or type-annotation issue.
You can look at the job output to figure out why it's failed or within a shell run:
```shell
$ scripts/check
```
It may be worth it to run `$ scripts/lint` to attempt auto-formatting the code
and if that job succeeds commit the changes.
### Docs Job Failed
This job failing means the documentation failed to build. This can happen for
a variety of reasons like invalid markdown or missing configuration within `mkdocs.yml`.
### Python 3.X Job Failed
This job failing means the unit tests failed or not all code paths are covered by unit tests.
If tests are failing you will see this message under the coverage report:
`=== 1 failed, 435 passed, 1 skipped, 1 xfailed in 11.09s ===`
If tests succeed but coverage doesn't reach our current threshold, you will see this
message under the coverage report:
`FAIL Required test coverage of 100% not reached. Total coverage: 99.00%`
## Releasing
*This section is targeted at HTTPX2 maintainers.*
Before releasing a new version, create a pull request that includes:
- **An update to the changelog**:
- We follow the format from [keepachangelog](https://keepachangelog.com/en/1.0.0/).
- [Compare](https://github.com/pydantic/httpx2/compare/) `main` with the tag of the latest release, and list all entries that are of interest to our users:
- Things that **must** go in the changelog: added, changed, deprecated or removed features, and bug fixes.
- Things that **should not** go in the changelog: changes to documentation, tests or tooling.
- Try sorting entries in descending order of impact / importance.
- Keep it concise and to-the-point. ๐ฏ
- **A version bump**: the version is derived automatically from git tags via `uv-dynamic-versioning` โ no file edit needed, just ensure the tag is applied after the release PR merges.
Once the release PR is merged, create a
[new release](https://github.com/pydantic/httpx2/releases/new) including:
- Tag version like `0.13.3`.
- Release title `Version 0.13.3`
- Description copied from the changelog.
Once created this release will be automatically uploaded to PyPI.
If something goes wrong with the PyPI job the release can be re-published using the
`scripts/publish` script.
## Development proxy setup
To test and debug requests via a proxy it's best to run a proxy server locally.
Any server should do but [`mitmproxy`](https://mitmproxy.org/) is a good
choice - it's written in Python, fully featured and has excellent UI and
tools for introspection of requests.
You can install `mitmproxy` using `pip install mitmproxy` or [several
other ways](https://docs.mitmproxy.org/stable/overview-installation/).
`mitmproxy` does require setting up local TLS certificates for HTTPS requests,
as its main purpose is to allow developers to inspect requests that pass through
it. We can set them up follows:
1. [`pip install trustme-cli`](https://github.com/sethmlarson/trustme-cli/).
2. `trustme-cli -i example.org www.example.org`, assuming you want to test
connecting to that domain, this will create three files: `server.pem`,
`server.key` and `client.pem`.
3. `mitmproxy` requires a PEM file that includes the private key and the
certificate so we need to concatenate them:
`cat server.key server.pem > server.withkey.pem`.
4. Start the proxy server `mitmproxy --certs server.withkey.pem`, or use the
[other mitmproxy commands](https://docs.mitmproxy.org/stable/) with different
UI options.
At this point the server is ready to start serving requests, you'll need to
configure HTTPX2 as described in the
[proxy section](https://httpx2.pydantic.dev/advanced/proxies/#http-proxies) and
the [SSL certificates section](https://httpx2.pydantic.dev/advanced/ssl/),
this is where our previously generated `client.pem` comes in:
```python
import ssl
import httpx2
ctx = ssl.create_default_context(cafile="/path/to/client.pem")
with httpx2.Client(proxy="http://127.0.0.1:8080/", verify=ctx) as client:
response = client.get("https://example.org")
print(response.status_code) # should print 200
```
Note, however, that HTTPS requests will only succeed to the host specified
in the SSL/TLS certificate we generated, HTTPS requests to other hosts will
raise an error like:
```
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate
verify failed: Hostname mismatch, certificate is not valid for
'duckduckgo.com'. (_ssl.c:1108)
```
If you want to make requests to more hosts you'll need to regenerate the
certificates and include all the hosts you intend to connect to in the
seconds step, i.e.
`trustme-cli -i example.org www.example.org duckduckgo.com www.duckduckgo.com`
pydantic-httpx2-6dd59b9/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 15265474634 0020747 5 ustar 00root root 0000000 0000000 pydantic-httpx2-6dd59b9/.github/ISSUE_TEMPLATE/1-issue.md 0000664 0000000 0000000 00000001052 15265474634 0022555 0 ustar 00root root 0000000 0000000 ---
name: Issue
about: Please only raise an issue if you've been advised to do so after discussion. Thanks! ๐
---
The starting point for issues should usually be a discussion...
https://github.com/pydantic/httpx2/discussions
Possible bugs and feature requests may be raised in the "Q&A" discussion category. We can then determine if the discussion needs to be escalated into an "Issue" or not.
This will help us ensure that the "Issues" list properly reflects ongoing or needed work on the project.
---
- [ ] Initially raised as discussion #...
pydantic-httpx2-6dd59b9/.github/ISSUE_TEMPLATE/config.yml 0000664 0000000 0000000 00000000773 15265474634 0022746 0 ustar 00root root 0000000 0000000 # Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser
blank_issues_enabled: false
contact_links:
- name: Discussions
url: https://github.com/pydantic/httpx2/discussions
about: >
The "Discussions" forum is where you want to start. ๐
- name: ๐ฌ Join Slack
url: https://logfire.pydantic.dev/docs/join-slack/
about: >
Join our Slack community to ask questions, get help, and chat about HTTPX2.
pydantic-httpx2-6dd59b9/.github/PULL_REQUEST_TEMPLATE.md 0000664 0000000 0000000 00000001040 15265474634 0022360 0 ustar 00root root 0000000 0000000
# Summary
# Checklist
- [ ] I understand that this PR may be closed in case there was no previous discussion. (This doesn't apply to typos!)
- [ ] I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change.
- [ ] I've updated the documentation accordingly.
pydantic-httpx2-6dd59b9/.github/dependabot.yml 0000664 0000000 0000000 00000000635 15265474634 0021420 0 ustar 00root root 0000000 0000000 version: 2
updates:
- package-ecosystem: "uv"
directory: "/"
schedule:
interval: "monthly"
cooldown:
default-days: 7
groups:
python-packages:
patterns:
- "*"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: monthly
cooldown:
default-days: 7
groups:
github-actions:
patterns:
- "*"
pydantic-httpx2-6dd59b9/.github/workflows/ 0000775 0000000 0000000 00000000000 15265474634 0020621 5 ustar 00root root 0000000 0000000 pydantic-httpx2-6dd59b9/.github/workflows/benchmark.yml 0000664 0000000 0000000 00000003043 15265474634 0023276 0 ustar 00root root 0000000 0000000 name: CodSpeed
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
permissions:
id-token: write
contents: read
jobs:
benchmarks:
name: Run benchmarks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
python-version: "3.13"
enable-cache: true
- name: Install dependencies
run: scripts/install
shell: bash
- name: Run the benchmarks
uses: CodSpeedHQ/action@373d6868929f444bc08d901fd0eb0ad52a8875ea # v5.2.1
with:
mode: instrumentation
run: uv run pytest tests/test_benchmark.py --codspeed
memory-benchmarks:
name: Run memory benchmarks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
python-version: "3.13"
enable-cache: true
- name: Install dependencies
run: scripts/install
shell: bash
- name: Run the benchmarks
uses: CodSpeedHQ/action@373d6868929f444bc08d901fd0eb0ad52a8875ea # v5.2.1
with:
mode: memory
run: uv run pytest tests/test_benchmark_memory.py --codspeed
pydantic-httpx2-6dd59b9/.github/workflows/main.yml 0000664 0000000 0000000 00000004073 15265474634 0022274 0 ustar 00root root 0000000 0000000 name: Test Suite
on:
push:
branches: ["main"]
pull_request:
branches: ["main", "version-*"]
permissions:
contents: read
jobs:
tests:
name: "Python ${{ matrix.python-version }}"
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
- name: Install dependencies
run: scripts/install
- name: Run linting checks
run: scripts/check
- name: Build package & docs
run: scripts/build
- name: Run tests
run: scripts/test
- name: Enforce coverage
run: scripts/coverage
check:
if: always()
needs: [tests]
runs-on: ubuntu-latest
steps:
- name: Decide whether the needed jobs succeeded or failed
uses: re-actors/alls-green@b5b5b37504aa4183270bd3d855c52a67f212be35 # v1.3.0
with:
jobs: ${{ toJSON(needs) }}
deploy-docs:
needs: [check]
if: success() && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Generate app token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
id: app-token
with:
app-id: ${{ vars.DOCS_APP_ID }}
private-key: ${{ secrets.DOCS_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: unified-docs
permission-contents: write
- name: Trigger unified-docs deployment
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
gh api repos/pydantic/unified-docs/dispatches \
--method POST \
-f event_type=docs-update \
-f "client_payload[source]=pydantic/httpx2@${{ github.sha }}"
pydantic-httpx2-6dd59b9/.github/workflows/publish.yml 0000664 0000000 0000000 00000002755 15265474634 0023023 0 ustar 00root root 0000000 0000000 name: Publish Python Package
on:
push:
tags:
- "**"
workflow_dispatch:
permissions: {}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0
- name: Install uv
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
enable-cache: false
- name: Set up Python
run: uv python install 3.12
- name: Install dependencies
run: scripts/install
- name: Build package & docs
run: scripts/build
- name: Upload package distributions
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: package-distributions
path: dist/
publish:
runs-on: ubuntu-latest
needs: build
permissions:
id-token: write
environment:
name: pypi
steps:
- name: Download package distributions
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: package-distributions
path: dist/
- name: Install uv
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
enable-cache: false
- name: Publish distributions to PyPI
run: uv publish --trusted-publishing always dist/*
pydantic-httpx2-6dd59b9/.github/workflows/zizmor.yml 0000664 0000000 0000000 00000001121 15265474634 0022671 0 ustar 00root root 0000000 0000000 name: GitHub Actions Security Analysis
on:
push:
branches: ["main"]
pull_request:
branches: ["**"]
permissions: {}
jobs:
zizmor:
runs-on: ubuntu-latest
permissions:
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Run zizmor ๐
uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4
pydantic-httpx2-6dd59b9/.gitignore 0000664 0000000 0000000 00000000203 15265474634 0017207 0 ustar 00root root 0000000 0000000 *.pyc
.coverage
.pytest_cache/
.mypy_cache/
__pycache__/
htmlcov/
site/
.wrangler/
*.egg-info/
venv*/
.python-version
build/
dist/
pydantic-httpx2-6dd59b9/AGENTS.md 0000664 0000000 0000000 00000000250 15265474634 0016524 0 ustar 00root root 0000000 0000000 - Do not force push.
- Use single tick (`) instead of double tick (``).
- Coding agents shouldn't co-author themselves. The authorship should be only for the engineer.
pydantic-httpx2-6dd59b9/LICENSE.md 0000664 0000000 0000000 00000003065 15265474634 0016634 0 ustar 00root root 0000000 0000000 Copyright ยฉ 2026 to present Pydantic Services Inc. and individual contributors.
Copyright ยฉ 2019, [Encode OSS Ltd](https://www.encode.io/).
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
pydantic-httpx2-6dd59b9/README.md 0000664 0000000 0000000 00000015424 15265474634 0016511 0 ustar 00root root 0000000 0000000 HTTPX2
A next-generation HTTP client for Python.
HTTPX2 is a fully featured HTTP client library for Python. It includes **an integrated command line client**, has support for both **HTTP/1.1 and HTTP/2**, and provides both **sync and async APIs**.
> [!NOTE]
> HTTPX2 is a continuation of the wonderful work started by [@lovelydinosaur](https://github.com/lovelydinosaur) and the broader HTTPX community. We're enormously grateful for everything that has gone into HTTPX over the years - it has been a foundational piece of the modern Python ecosystem, and this project would not exist without it.
>
> With HTTPX itself seeing limited activity recently, Pydantic is picking up stewardship under the HTTPX2 name so that users have a reliably maintained path forward - including timely security updates for a library that sits in the critical path of so many production systems. Our aim is to honour the original project's design, keep it stable for everyone relying on it, and continue evolving it carefully. Thank you to [@lovelydinosaur](https://github.com/lovelydinosaur) and every past contributor for laying such a strong foundation. ๐
---
Install HTTPX2 using pip:
```shell
pip install httpx2
```
Now, let's get started:
```pycon
>>> import httpx2
>>> r = httpx2.get('https://www.example.org/')
>>> r
>>> r.status_code
200
>>> r.headers['content-type']
'text/html; charset=UTF-8'
>>> r.text
'\n\n\nExample Domain...'
```
Or, using the command-line client.
```shell
pip install 'httpx2[cli]' # The command line client is an optional dependency.
```
Which now allows us to use HTTPX2 directly from the command-line:
```shell
httpx2 --help
```
## Features
HTTPX2 builds on the well-established usability of `requests`, and gives you:
* A broadly [requests-compatible API](https://pydantic.dev/docs/httpx2/guides/compatibility/).
* An integrated command-line client.
* HTTP/1.1 [and HTTP/2 support](https://pydantic.dev/docs/httpx2/guides/http2/).
* Standard synchronous interface, but with [async support if you need it](https://pydantic.dev/docs/httpx2/guides/async/).
* Ability to make requests directly to [WSGI applications](https://pydantic.dev/docs/httpx2/advanced/transports/#wsgi-transport) or [ASGI applications](https://pydantic.dev/docs/httpx2/advanced/transports/#asgi-transport).
* Strict timeouts everywhere.
* Fully type annotated.
* 100% test coverage.
Plus all the standard features of `requests`...
* International Domains and URLs
* Keep-Alive & Connection Pooling
* Sessions with Cookie Persistence
* Browser-style SSL Verification
* Basic/Digest Authentication
* Elegant Key/Value Cookies
* Automatic Decompression
* Automatic Content Decoding
* Unicode Response Bodies
* Multipart File Uploads
* HTTP(S) Proxy Support
* Connection Timeouts
* Streaming Downloads
* .netrc Support
* Chunked Requests
## Installation
Install with pip:
```shell
pip install httpx2
```
Or, to include the optional HTTP/2 support, use:
```shell
pip install httpx2[http2]
```
## Documentation
Project documentation is available at [https://pydantic.dev/docs/httpx2/](https://pydantic.dev/docs/httpx2/).
For a run-through of all the basics, head over to the [QuickStart](https://pydantic.dev/docs/httpx2/get-started/quickstart/).
For more advanced topics, see the [Advanced Usage](https://pydantic.dev/docs/httpx2/advanced/clients/) section, the [async support](https://pydantic.dev/docs/httpx2/guides/async/) section, or the [HTTP/2](https://pydantic.dev/docs/httpx2/guides/http2/) section.
The [Developer Interface](https://pydantic.dev/docs/httpx2/api/api/) provides a comprehensive API reference.
To find out about tools that integrate with HTTPX, see [Third Party Packages](https://pydantic.dev/docs/httpx2/community/third_party_packages/).
## Observability with Pydantic Logfire
Install Logfire with HTTPX instrumentation support:
```shell
pip install 'logfire[httpx]'
```
HTTPX2 works out of the box with [Pydantic Logfire](https://pydantic.dev/logfire), our observability platform built on OpenTelemetry. One line instruments every request, giving you traces, timings, and status codes with no other changes:
```python
import logfire
import httpx2
logfire.configure()
logfire.instrument_httpx()
httpx2.get("https://pydantic.dev/")
```
This works the same way for explicit `httpx2.Client()` and `httpx2.AsyncClient()` instances, and you can scope instrumentation to a single client by passing it in: `logfire.instrument_httpx(client)`. Pass `capture_all=True` to also record headers and bodies. See the [Logfire HTTPX docs](https://logfire.pydantic.dev/docs/integrations/http-clients/httpx/) for the full set of options.
## Contribute
If you want to contribute with HTTPX2 check out the [Contributing Guide](https://pydantic.dev/docs/httpx2/community/contributing/) to learn how to start.
## Dependencies
The HTTPX2 project relies on these excellent libraries:
* `httpcore2` - The underlying transport implementation for `httpx2`.
* `h11` - HTTP/1.1 support.
* `anyio` - Structured concurrency primitives, used to support both `asyncio` and `trio`.
* `truststore` - SSL verification using the operating system's trust store.
* `idna` - Internationalized domain name support.
As well as these optional installs:
* `h2` - HTTP/2 support. *(Optional, with `httpx2[http2]`)*
* `socksio` - SOCKS proxy support. *(Optional, with `httpx2[socks]`)*
* `rich` - Rich terminal support. *(Optional, with `httpx2[cli]`)*
* `click` - Command line client support. *(Optional, with `httpx2[cli]`)*
* `brotli` or `brotlicffi` - Decoding for "brotli" compressed responses. *(Optional, with `httpx2[brotli]`)*
* `backports.zstd` - Decoding for "zstd" compressed responses on Python 3.13 and below. *(Optional, with `httpx2[zstd]`. On Python 3.14+, `zstd` is supported via the stdlib [`compression.zstd`](https://docs.python.org/3/library/compression.zstd.html) module when it is available, and the `httpx2[zstd]` extra installs nothing.)*
A huge amount of credit is due to `requests` for the API layout that
much of this work follows, as well as to `urllib3` for plenty of design
inspiration around the lower-level networking details.
---
HTTPX2 is BSD licensed code.
Designed & crafted with care.
— ๐ฆ —
pydantic-httpx2-6dd59b9/benchmark/ 0000775 0000000 0000000 00000000000 15265474634 0017156 5 ustar 00root root 0000000 0000000 pydantic-httpx2-6dd59b9/benchmark/README.md 0000664 0000000 0000000 00000005167 15265474634 0020446 0 ustar 00root root 0000000 0000000 # Throughput benchmark
An end-to-end, keep-alive throughput benchmark for the async client stack, designed so that the client
under test is the bottleneck rather than the server.
* `server.py` is a raw `asyncio.Protocol` HTTP/1.1 origin: `GET /` returns `n` bytes, `POST /echo`
echoes the body. No parsing library, no per-request sleep. The server and clients run on
[zuvloop](https://github.com/Kludex/zuvloop) when it is importable (`--no-zuvloop` for the stdlib loop).
* `client.py` runs one library at one concurrency and body size, and prints a JSON line with rps,
p50/p99 latency and per-request CPU time.
* `run.py` starts the server, runs the matrix interleaved across interpreters and libraries, and
prints a comparison table plus two headline numbers.
```
scripts/benchmark # default matrix, httpx2 vs aiohttp
scripts/benchmark --quick # c16/1k, c512/1k, c64/100k
scripts/benchmark --lib httpx2 --lib httpcore2 --scenario c512/1k --rounds 3
scripts/benchmark --profile /tmp/profiles # pyinstrument output per scenario
```
## Comparing two revisions
Install each revision in its own virtual environment and pass both interpreters. Rounds are interleaved,
so both columns see the same machine noise:
```
git worktree add ../httpx2-main main && (cd ../httpx2-main && uv sync)
scripts/benchmark --python main=../httpx2-main/.venv/bin/python --python work=.venv/bin/python --lib httpx2
```
`punkreq` is supported as an extra reference point (`--lib punkreq`) if it is installed.
## Reading the results
* `us/req` is CPU time per request, measured with `time.process_time()`, and is more stable than rps on
noisy machines.
* The `stream` mode (default) mirrors a proxy hot path: a hand-built `Request`, `send(stream=True)`, and
`aiter_raw(256 KiB)`. Use `--mode read` for `client.request(...)` with the body read in full.
* `retention` is rps at the highest 1KB GET concurrency divided by rps at the lowest. A client whose
per-request cost grows with the number of pooled connections has low retention.
* Pools are unbounded by default (`--max-connections` to bound them), and the server and clients are
pinned to separate CPUs when at least two are available (`--no-pin` to disable).
Results depend on the interpreter: the table header shows the version and whether the build is
free-threaded. Free-threaded builds are markedly slower on this workload, so use a regular CPython
build for representative numbers, for example `uv sync --python 3.14` before `scripts/benchmark`.
zuvloop requires CPython 3.14 or later; on other interpreters the harness uses the stdlib loop and the
JSON output records `"zuvloop": false`.
pydantic-httpx2-6dd59b9/benchmark/client.py 0000664 0000000 0000000 00000030452 15265474634 0021012 0 ustar 00root root 0000000 0000000 """
Single benchmark worker: drives one client library at one concurrency and body size, then emits a JSON line.
Normally launched by `run.py`, but it can be used directly against a running `server.py`:
python client.py --lib httpx2 --concurrency 16 --size 1024
Modes:
* `stream` (default) mirrors a proxy hot path: a hand-built request, `stream=True`, raw chunk iteration.
* `read` is the plain "request and read the whole body" path.
"""
from __future__ import annotations
import argparse
import asyncio
import json
import os
import sys
import time
from collections.abc import AsyncIterable, AsyncIterator, Awaitable, Callable, Coroutine
from dataclasses import dataclass
from typing import Any, TypeVar
DEFAULT_PORT = 8765
DEFAULT_CHUNK_SIZE = 256 * 1024
KEEPALIVE_EXPIRY = 95.0
CONNECT_TIMEOUT = 5.0
READ_TIMEOUT = 60.0
T = TypeVar("T")
RequestFn = Callable[[], Awaitable[None]]
CloseFn = Callable[[], Awaitable[None]]
Builder = Callable[["Scenario", bytes], tuple[RequestFn, CloseFn]]
@dataclass(frozen=True)
class Scenario:
lib: str
concurrency: int
size: int
post: bool
mode: str
chunk_size: int
max_connections: int | None
host: str
port: int
@property
def method(self) -> str:
return "POST" if self.post else "GET"
@property
def url(self) -> str:
path = "/echo" if self.post else f"/{self.size}"
return f"http://{self.host}:{self.port}{path}"
def check_size(received: int, expected: int) -> None:
if received != expected:
raise RuntimeError(f"Expected a {expected} byte body, received {received} bytes.")
def build_httpx2(scenario: Scenario, payload: bytes) -> tuple[RequestFn, CloseFn]:
import httpx2
client = httpx2.AsyncClient(
limits=httpx2.Limits(
max_connections=scenario.max_connections,
max_keepalive_connections=None,
keepalive_expiry=KEEPALIVE_EXPIRY,
),
timeout=httpx2.Timeout(CONNECT_TIMEOUT, read=READ_TIMEOUT),
# Keep the benchmark direct even if HTTP_PROXY or ALL_PROXY is set in the environment.
trust_env=False,
)
headers = [("host", scenario.host), ("user-agent", "httpx2-benchmark")]
if scenario.post:
headers.append(("content-length", str(len(payload))))
class RequestBody(httpx2.AsyncByteStream):
async def __aiter__(self) -> AsyncIterator[bytes]:
if scenario.post:
yield payload
async def stream_one() -> None:
request = httpx2.Request(scenario.method, scenario.url, headers=headers, stream=RequestBody())
response = await client.send(request, stream=True, follow_redirects=False)
received = 0
async for chunk in response.aiter_raw(scenario.chunk_size):
received += len(chunk)
check_size(received, scenario.size)
async def read_one() -> None:
response = await client.request(scenario.method, scenario.url, content=payload if scenario.post else None)
check_size(len(response.content), scenario.size)
return (stream_one if scenario.mode == "stream" else read_one), client.aclose
def build_httpcore2(scenario: Scenario, payload: bytes) -> tuple[RequestFn, CloseFn]:
import httpcore2
pool = httpcore2.AsyncConnectionPool(
max_connections=scenario.max_connections,
max_keepalive_connections=None,
keepalive_expiry=KEEPALIVE_EXPIRY,
)
extensions: dict[str, Any] = {
"timeout": {"connect": CONNECT_TIMEOUT, "read": READ_TIMEOUT, "write": CONNECT_TIMEOUT, "pool": CONNECT_TIMEOUT}
}
content = payload if scenario.post else None
async def stream_one() -> None:
async with pool.stream(scenario.method, scenario.url, content=content, extensions=extensions) as response:
assert isinstance(response.stream, AsyncIterable)
received = 0
async for chunk in response.stream:
received += len(chunk)
check_size(received, scenario.size)
async def read_one() -> None:
response = await pool.request(scenario.method, scenario.url, content=content, extensions=extensions)
check_size(len(response.content), scenario.size)
return (stream_one if scenario.mode == "stream" else read_one), pool.aclose
def build_aiohttp(scenario: Scenario, payload: bytes) -> tuple[RequestFn, CloseFn]:
import aiohttp
session = aiohttp.ClientSession(
connector=aiohttp.TCPConnector(limit=scenario.max_connections or 0, keepalive_timeout=KEEPALIVE_EXPIRY),
timeout=aiohttp.ClientTimeout(connect=CONNECT_TIMEOUT, sock_read=READ_TIMEOUT),
)
data = payload if scenario.post else None
async def stream_one() -> None:
async with session.request(scenario.method, scenario.url, data=data) as response:
received = 0
async for chunk in response.content.iter_chunked(scenario.chunk_size):
received += len(chunk)
check_size(received, scenario.size)
async def read_one() -> None:
async with session.request(scenario.method, scenario.url, data=data) as response:
body = await response.read()
check_size(len(body), scenario.size)
return (stream_one if scenario.mode == "stream" else read_one), session.close
def build_punkreq(scenario: Scenario, payload: bytes) -> tuple[RequestFn, CloseFn]:
import punkreq
from punkreq.asyncio import Client
client = Client(
limits=punkreq.Limits(
max_connections=scenario.max_connections,
max_keepalive_connections=None,
keepalive_expiry=KEEPALIVE_EXPIRY,
),
timeout=punkreq.Timeout(connect=CONNECT_TIMEOUT, read=READ_TIMEOUT, pool=CONNECT_TIMEOUT, total=None),
follow_redirects=False,
http2=False,
trust_env=False,
)
headers = [("host", scenario.host), ("user-agent", "httpx2-benchmark")]
if scenario.post:
headers.append(("content-length", str(len(payload))))
async def request_body(self: object) -> AsyncIterator[bytes]:
yield payload
# Built dynamically so this module type-checks when punkreq is not installed.
request_body_stream = type("RequestBody", (punkreq.AsyncByteStream,), {"__aiter__": request_body})
async def stream_one() -> None:
request = punkreq.Request(
scenario.method,
scenario.url,
headers=headers,
stream=request_body_stream() if scenario.post else None,
)
response = await client.send(request, follow_redirects=False)
received = 0
async for chunk in response.iter_raw(scenario.chunk_size):
received += len(chunk)
check_size(received, scenario.size)
async def read_one() -> None:
response = await client.request(scenario.method, scenario.url, content=payload if scenario.post else None)
check_size(len(await response.read()), scenario.size)
return (stream_one if scenario.mode == "stream" else read_one), client.close
BUILDERS: dict[str, Builder] = {
"httpx2": build_httpx2,
"httpcore2": build_httpcore2,
"aiohttp": build_aiohttp,
"punkreq": build_punkreq,
}
async def drive(one: RequestFn, concurrency: int, seconds: float) -> list[float]:
deadline = time.perf_counter() + seconds
latencies: list[float] = []
async def worker() -> None:
while True:
start = time.perf_counter()
await one()
end = time.perf_counter()
latencies.append(end - start)
if end >= deadline:
return
await asyncio.gather(*(worker() for _ in range(concurrency)))
return latencies
def percentile(sorted_values: list[float], fraction: float) -> float:
return sorted_values[min(len(sorted_values) - 1, int(len(sorted_values) * fraction))]
async def run_scenario(
scenario: Scenario, seconds: float, warmup_seconds: float, profile_path: str | None
) -> dict[str, Any]:
payload = b"y" * scenario.size if scenario.post else b""
one, aclose = BUILDERS[scenario.lib](scenario, payload)
try:
# Establish `concurrency` connections, then run unmeasured for a while.
await asyncio.gather(*(one() for _ in range(scenario.concurrency)))
await drive(one, scenario.concurrency, warmup_seconds)
profiler = None
if profile_path is not None:
import pyinstrument
profiler = pyinstrument.Profiler(async_mode="disabled")
profiler.start()
cpu_start = time.process_time()
wall_start = time.perf_counter()
latencies = await drive(one, scenario.concurrency, seconds)
wall = time.perf_counter() - wall_start
cpu = time.process_time() - cpu_start
if profiler is not None:
profiler.stop()
with open(profile_path, "w") as f: # type: ignore[arg-type]
f.write(profiler.output_text(unicode=True, color=False))
finally:
await aclose()
latencies.sort()
count = len(latencies)
return {
"lib": scenario.lib,
"mode": scenario.mode,
"concurrency": scenario.concurrency,
"size": scenario.size,
"post": scenario.post,
"requests": count,
"seconds": round(wall, 3),
"rps": round(count / wall, 1),
"p50_ms": round(percentile(latencies, 0.5) * 1000, 2),
"p99_ms": round(percentile(latencies, 0.99) * 1000, 2),
"max_ms": round(latencies[-1] * 1000, 2),
"cpu_us": round(cpu * 1_000_000 / count, 1),
}
def pin_to_cpu(cpu: int | None) -> None:
if cpu is not None and hasattr(os, "sched_setaffinity"):
os.sched_setaffinity(0, {cpu})
def run(coro: Coroutine[Any, Any, T], use_zuvloop: bool) -> tuple[T, bool]:
loop: asyncio.AbstractEventLoop
zuvloop_active = False
if use_zuvloop:
try:
import zuvloop
except ImportError:
loop = asyncio.new_event_loop()
else:
loop = zuvloop.new_event_loop()
zuvloop_active = True
else:
loop = asyncio.new_event_loop()
try:
return loop.run_until_complete(coro), zuvloop_active
finally:
loop.run_until_complete(loop.shutdown_asyncgens())
loop.close()
def interpreter_info() -> dict[str, Any]:
gil_enabled = True
if sys.version_info >= (3, 13):
gil_enabled = sys._is_gil_enabled()
return {"python": sys.version.split()[0], "gil": gil_enabled}
def main(argv: list[str] | None = None) -> None:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--lib", choices=sorted(BUILDERS), required=True)
parser.add_argument("--concurrency", type=int, default=16)
parser.add_argument("--size", type=int, default=1024, help="Response body size in bytes (request body for --post).")
parser.add_argument("--post", action="store_true", help="POST a body of --size bytes to the echo endpoint.")
parser.add_argument("--mode", choices=("stream", "read"), default="stream")
parser.add_argument("--chunk-size", type=int, default=DEFAULT_CHUNK_SIZE)
parser.add_argument("--max-connections", type=int, default=None, help="Pool limit; unbounded by default.")
parser.add_argument("--seconds", type=float, default=4.0, help="Measured duration.")
parser.add_argument("--warmup", type=float, default=0.5, help="Unmeasured duration before measuring.")
parser.add_argument("--host", default="127.0.0.1")
parser.add_argument("--port", type=int, default=DEFAULT_PORT)
parser.add_argument("--cpu", type=int, default=None, help="Pin this worker to a CPU (Linux only).")
parser.add_argument("--no-zuvloop", action="store_true")
parser.add_argument("--profile", default=None, help="Write a pyinstrument profile of the measured phase here.")
args = parser.parse_args(argv)
pin_to_cpu(args.cpu)
scenario = Scenario(
lib=args.lib,
concurrency=args.concurrency,
size=args.size,
post=args.post,
mode=args.mode,
chunk_size=args.chunk_size,
max_connections=args.max_connections,
host=args.host,
port=args.port,
)
result, zuvloop_active = run(
run_scenario(scenario, args.seconds, args.warmup, args.profile), use_zuvloop=not args.no_zuvloop
)
result.update(interpreter_info(), zuvloop=zuvloop_active)
print(json.dumps(result), flush=True)
if __name__ == "__main__":
main()
pydantic-httpx2-6dd59b9/benchmark/run.py 0000664 0000000 0000000 00000031172 15265474634 0020340 0 ustar 00root root 0000000 0000000 """
Benchmark orchestrator: starts the origin server, runs the client matrix, and prints a comparison table.
Examples:
python run.py # default matrix, current interpreter, httpx2 vs aiohttp
python run.py --quick --lib httpx2 # three scenarios, one library
python run.py --python main=../httpx2-main/.venv/bin/python --python work=.venv/bin/python
Each `--python LABEL=PATH` is an interpreter with the libraries installed; runs are interleaved across
interpreters and libraries on every round so machine noise affects all columns equally.
Two headline numbers are reported per column:
* `cpu us/req` at the smallest 1KB GET scenario: per-request CPU cost of the client stack.
* `retention`: throughput at the largest 1KB GET concurrency divided by throughput at the smallest.
"""
from __future__ import annotations
import argparse
import json
import os
import pathlib
import queue
import statistics
import subprocess
import sys
import threading
from dataclasses import dataclass
from typing import Any
HERE = pathlib.Path(__file__).parent
SERVER = HERE / "server.py"
CLIENT = HERE / "client.py"
DEFAULT_PORT = 8765
DEFAULT_SCENARIOS = ["c1/1k", "c16/1k", "c128/1k", "c512/1k", "c64/100k", "c8/5m", "c64/1k/post", "c8/1m/post"]
QUICK_SCENARIOS = ["c16/1k", "c512/1k", "c64/100k"]
PROFILE_LIBS = ("httpx2", "httpcore2")
@dataclass(frozen=True)
class ScenarioSpec:
concurrency: int
size: int
post: bool
@classmethod
def parse(cls, text: str) -> ScenarioSpec:
parts = text.split("/")
valid = len(parts) in (2, 3) and parts[0].startswith("c") and (len(parts) == 2 or parts[2] == "post")
if not valid:
raise argparse.ArgumentTypeError(f"Expected 'c/[/post]', got {text!r}.")
return cls(concurrency=int(parts[0][1:]), size=parse_size(parts[1]), post=len(parts) == 3)
@property
def label(self) -> str:
return f"c{self.concurrency}/{format_size(self.size)}" + ("/post" if self.post else "")
def parse_size(text: str) -> int:
units = {"k": 1024, "m": 1024 * 1024}
unit = text[-1].lower()
if unit in units:
return int(text[:-1]) * units[unit]
return int(text)
def format_size(size: int) -> str:
if size % (1024 * 1024) == 0:
return f"{size // (1024 * 1024)}m"
if size % 1024 == 0:
return f"{size // 1024}k"
return str(size)
def available_cpus() -> list[int]:
# Respect the affinity mask: in containers the usable CPU IDs need not start at zero.
if hasattr(os, "sched_getaffinity"):
return sorted(os.sched_getaffinity(0))
return list(range(os.cpu_count() or 1))
def parse_python(text: str) -> tuple[str, str]:
label, sep, path = text.partition("=")
if not sep:
raise argparse.ArgumentTypeError(f"Expected 'LABEL=PATH', got {text!r}.")
return label, path
def wait_for_server(server: subprocess.Popen[bytes], port: int, timeout: float = 10.0) -> None:
# Wait for the server's readiness line rather than probing the port, so a failed
# bind (for example, the port already in use) is an error instead of a silent
# benchmark against whatever else is listening there.
assert server.stdout is not None
lines: queue.Queue[bytes] = queue.Queue()
threading.Thread(target=lambda: lines.put(server.stdout.readline()), daemon=True).start() # type: ignore[union-attr]
try:
line = lines.get(timeout=timeout)
except queue.Empty:
raise RuntimeError(f"Benchmark server did not start listening on port {port} within {timeout}s.") from None
if not line.startswith(b"listening on "):
code = server.wait(timeout=5)
raise RuntimeError(f"Benchmark server exited with code {code}; is port {port} already in use?")
def interpreter_summary(python: str) -> str:
code = (
"import sys, json; gil = sys._is_gil_enabled() if sys.version_info >= (3, 13) else True; "
"print(json.dumps({'python': sys.version.split()[0], 'gil': gil}))"
)
info = json.loads(subprocess.run([python, "-c", code], check=True, capture_output=True, text=True).stdout)
return f"{info['python']}{'' if info['gil'] else ' (free-threaded)'}"
def run_worker(
python: str, lib: str, spec: ScenarioSpec, args: argparse.Namespace, profile: pathlib.Path | None = None
) -> dict[str, Any]:
command = [
python,
str(CLIENT),
"--lib",
lib,
"--concurrency",
str(spec.concurrency),
"--size",
str(spec.size),
"--mode",
args.mode,
"--seconds",
str(args.seconds),
"--port",
str(args.port),
]
if spec.post:
command.append("--post")
if args.max_connections is not None:
command += ["--max-connections", str(args.max_connections)]
if args.client_cpu is not None:
command += ["--cpu", str(args.client_cpu)]
if args.no_zuvloop:
command.append("--no-zuvloop")
if profile is not None:
command += ["--profile", str(profile)]
completed = subprocess.run(command, capture_output=True, text=True)
if completed.returncode != 0:
tail = "\n".join(completed.stderr.strip().splitlines()[-3:])
return {"error": tail or f"exit code {completed.returncode}"}
result: dict[str, Any] = json.loads(completed.stdout.strip().splitlines()[-1])
return result
def median(results: list[dict[str, Any]], key: str) -> float:
return float(statistics.median(r[key] for r in results))
def print_table(
specs: list[ScenarioSpec], columns: list[str], results: dict[tuple[str, str], list[dict[str, Any]]]
) -> None:
header = ["scenario", *columns]
rows = []
for spec in specs:
row = [spec.label]
for column in columns:
ok = [r for r in results.get((spec.label, column), []) if "error" not in r]
if not ok:
errors = [r["error"] for r in results.get((spec.label, column), []) if "error" in r]
row.append(f"FAILED: {errors[0]}" if errors else "-")
continue
row.append(
f"{median(ok, 'rps'):,.0f} rps ยท {median(ok, 'p50_ms'):.1f}/{median(ok, 'p99_ms'):.1f} ms"
f" ยท {median(ok, 'cpu_us'):.0f} us"
)
rows.append(row)
widths = [max(len(line[i]) for line in [header, *rows]) for i in range(len(header))]
print("Cells: median rps ยท p50/p99 latency ms ยท CPU us per request")
print("| " + " | ".join(cell.ljust(widths[i]) for i, cell in enumerate(header)) + " |")
print("|" + "|".join("-" * (width + 2) for width in widths) + "|")
for row in rows:
print("| " + " | ".join(cell.ljust(widths[i]) for i, cell in enumerate(row)) + " |")
def print_kpis(
specs: list[ScenarioSpec], columns: list[str], results: dict[tuple[str, str], list[dict[str, Any]]]
) -> None:
get_1k = sorted((s for s in specs if s.size == 1024 and not s.post), key=lambda s: s.concurrency)
if not get_1k:
return
low, high = get_1k[0], get_1k[-1]
print()
if high is low:
print(f"KPIs (cpu at {low.label}):")
else:
print(f"KPIs (cpu at {low.label}, retention = rps {high.label} / rps {low.label}):")
for column in columns:
low_ok = [r for r in results.get((low.label, column), []) if "error" not in r]
high_ok = [r for r in results.get((high.label, column), []) if "error" not in r]
if not low_ok:
continue
line = f" {column}: {median(low_ok, 'cpu_us'):.0f} us/req"
if high_ok and high is not low:
line += f", retention {median(high_ok, 'rps') / median(low_ok, 'rps'):.0%}"
print(line)
def main(argv: list[str] | None = None) -> None:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument(
"--python", type=parse_python, action="append", default=[], metavar="LABEL=PATH", help="Interpreter to run."
)
parser.add_argument(
"--lib", action="append", default=[], help="Client library (repeatable). Default: httpx2, aiohttp."
)
parser.add_argument(
"--scenario", type=ScenarioSpec.parse, action="append", default=[], metavar="c/[/post]"
)
parser.add_argument("--quick", action="store_true", help=f"Use the short matrix: {', '.join(QUICK_SCENARIOS)}.")
parser.add_argument("--mode", choices=("stream", "read"), default="stream")
parser.add_argument("--rounds", type=int, default=2, help="Interleaved repetitions; medians are reported.")
parser.add_argument("--seconds", type=float, default=4.0, help="Measured duration per run.")
parser.add_argument("--max-connections", type=int, default=None)
parser.add_argument("--port", type=int, default=DEFAULT_PORT)
parser.add_argument(
"--server-cpu", type=int, default=None, help="Default: the first usable CPU when at least two are available."
)
parser.add_argument(
"--client-cpu", type=int, default=None, help="Default: the second usable CPU when at least two are available."
)
parser.add_argument("--no-pin", action="store_true", help="Do not pin the server and clients to CPUs.")
parser.add_argument(
"--no-zuvloop", action="store_true", help="Use the stdlib event loop for the server and clients."
)
parser.add_argument("--profile", type=pathlib.Path, default=None, help="Directory for pyinstrument profiles.")
parser.add_argument("--output", type=pathlib.Path, default=None, help="Write raw results as JSON.")
args = parser.parse_args(argv)
pythons: list[tuple[str, str]] = args.python or [("current", sys.executable)]
if len({label for label, _ in pythons}) != len(pythons):
parser.error("--python labels must be unique.")
libs: list[str] = list(dict.fromkeys(args.lib or ["httpx2", "aiohttp"]))
# Deduplicate by value, so `c16/1k` and `c16/1024` do not become two rows sharing one bucket.
specs: list[ScenarioSpec] = list(
dict.fromkeys(
args.scenario or [ScenarioSpec.parse(s) for s in (QUICK_SCENARIOS if args.quick else DEFAULT_SCENARIOS)]
)
)
cpus = available_cpus()
if not args.no_pin and len(cpus) >= 2:
args.server_cpu = cpus[0] if args.server_cpu is None else args.server_cpu
args.client_cpu = cpus[1] if args.client_cpu is None else args.client_cpu
columns = [f"{lib}@{label}" if len(pythons) > 1 else lib for label, _ in pythons for lib in libs]
print(f"cpus: {len(cpus)}, server cpu: {args.server_cpu}, client cpu: {args.client_cpu}, mode: {args.mode}")
for label, python in pythons:
print(f"{label}: {python} ({interpreter_summary(python)})")
print(f"rounds: {args.rounds}, {args.seconds}s per run, {len(specs)} scenarios, {len(columns)} columns")
print()
server_command = [sys.executable, str(SERVER), "--port", str(args.port)]
if args.server_cpu is not None:
server_command += ["--cpu", str(args.server_cpu)]
if args.no_zuvloop:
server_command.append("--no-zuvloop")
server = subprocess.Popen(server_command, stdout=subprocess.PIPE)
results: dict[tuple[str, str], list[dict[str, Any]]] = {}
try:
wait_for_server(server, args.port)
for round_number in range(1, args.rounds + 1):
for spec in specs:
for label, python in pythons:
for lib in libs:
column = f"{lib}@{label}" if len(pythons) > 1 else lib
result = run_worker(python, lib, spec, args)
results.setdefault((spec.label, column), []).append(result)
summary = result.get("error") or f"{result['rps']:,.0f} rps, {result['cpu_us']:.0f} us/req"
print(f"[round {round_number}] {spec.label:<14} {column:<24} {summary}", flush=True)
if args.profile is not None:
args.profile.mkdir(parents=True, exist_ok=True)
label, python = pythons[0]
for spec in specs:
for lib in libs:
if lib not in PROFILE_LIBS:
continue
path = args.profile / f"{lib}-{label}-{spec.label.replace('/', '-')}.txt"
result = run_worker(python, lib, spec, args, profile=path)
print(f"[profile] {spec.label:<14} {lib:<24} {result.get('error') or path}", flush=True)
finally:
server.terminate()
server.wait(timeout=5)
print()
print_table(specs, columns, results)
print_kpis(specs, columns, results)
if args.output is not None:
args.output.write_text(json.dumps({f"{k[0]} {k[1]}": v for k, v in results.items()}, indent=2))
print(f"\nraw results: {args.output}")
if __name__ == "__main__":
main()
pydantic-httpx2-6dd59b9/benchmark/server.py 0000664 0000000 0000000 00000010332 15265474634 0021035 0 ustar 00root root 0000000 0000000 """
Minimal HTTP/1.1 keep-alive origin for the benchmark harness.
The server is deliberately trivial so that the client under test is the bottleneck:
* `GET /` responds with an `n`-byte body.
* `POST /echo` echoes the request body.
Usage: `python server.py [--port 8765] [--cpu N] [--no-zuvloop]`
"""
from __future__ import annotations
import argparse
import asyncio
import os
import sys
from collections.abc import Coroutine
from typing import Any, TypeVar, cast
DEFAULT_PORT = 8765
RESPONSE_HEAD = b"HTTP/1.1 200 OK\r\ncontent-type: application/octet-stream\r\ncontent-length: %d\r\n\r\n"
BAD_REQUEST = b"HTTP/1.1 400 Bad Request\r\ncontent-length: 0\r\n\r\n"
MAX_BODY_SIZE = 64 * 1024 * 1024
T = TypeVar("T")
_bodies: dict[int, bytes] = {}
def response_body(target: bytes) -> bytes | None:
try:
size = int(target.rsplit(b"/", 1)[-1] or b"0")
except ValueError:
return None
if not 0 <= size <= MAX_BODY_SIZE:
return None
body = _bodies.get(size)
if body is None:
body = _bodies[size] = b"x" * size
return body
class OriginProtocol(asyncio.Protocol):
def __init__(self) -> None:
self._buffer = bytearray()
self._transport: asyncio.Transport | None = None
def connection_made(self, transport: asyncio.BaseTransport) -> None:
# zuvloop transports implement the interface without subclassing `asyncio.Transport`.
self._transport = cast(asyncio.Transport, transport)
def data_received(self, data: bytes) -> None:
self._buffer += data
while self._respond_once():
pass
def _respond_once(self) -> bool:
head_end = self._buffer.find(b"\r\n\r\n")
if head_end < 0:
return False
request_line, _, header_block = bytes(self._buffer[:head_end]).partition(b"\r\n")
method, target, _ = request_line.split(b" ", 2)
content_length = 0
for line in header_block.split(b"\r\n"):
name, _, value = line.partition(b":")
if name.strip().lower() == b"content-length":
content_length = int(value)
total = head_end + 4 + content_length
if len(self._buffer) < total:
return False
body = bytes(self._buffer[head_end + 4 : total])
del self._buffer[:total]
payload = body if method == b"POST" else response_body(target)
assert self._transport is not None
if payload is None:
self._transport.write(BAD_REQUEST)
else:
self._transport.writelines([RESPONSE_HEAD % len(payload), payload])
return True
async def serve(port: int) -> None:
loop = asyncio.get_running_loop()
server = await loop.create_server(OriginProtocol, "127.0.0.1", port, backlog=4096)
# The orchestrator waits for this line; a bind failure ends stdout without it.
print(f"listening on 127.0.0.1:{port}", flush=True)
async with server:
await server.serve_forever()
def pin_to_cpu(cpu: int | None) -> None:
if cpu is not None and hasattr(os, "sched_setaffinity"):
os.sched_setaffinity(0, {cpu})
def run(coro: Coroutine[Any, Any, T], use_zuvloop: bool) -> T:
loop: asyncio.AbstractEventLoop
if use_zuvloop:
try:
import zuvloop
except ImportError:
loop = asyncio.new_event_loop()
else:
loop = zuvloop.new_event_loop()
else:
loop = asyncio.new_event_loop()
try:
return loop.run_until_complete(coro)
finally:
loop.run_until_complete(loop.shutdown_asyncgens())
loop.close()
def main(argv: list[str] | None = None) -> None:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--port", type=int, default=DEFAULT_PORT)
parser.add_argument("--cpu", type=int, default=None, help="Pin the server to this CPU (Linux only).")
parser.add_argument(
"--no-zuvloop", action="store_true", help="Use the stdlib event loop even if zuvloop is installed."
)
args = parser.parse_args(argv)
pin_to_cpu(args.cpu)
try:
run(serve(args.port), use_zuvloop=not args.no_zuvloop)
except KeyboardInterrupt:
sys.exit(0)
if __name__ == "__main__":
main()
pydantic-httpx2-6dd59b9/docs/ 0000775 0000000 0000000 00000000000 15265474634 0016154 5 ustar 00root root 0000000 0000000 pydantic-httpx2-6dd59b9/docs/advanced/ 0000775 0000000 0000000 00000000000 15265474634 0017721 5 ustar 00root root 0000000 0000000 pydantic-httpx2-6dd59b9/docs/advanced/authentication.md 0000664 0000000 0000000 00000020613 15265474634 0023264 0 ustar 00root root 0000000 0000000 Authentication can either be included on a per-request basis...
```pycon
>>> auth = httpx2.BasicAuth(username="username", password="secret")
>>> client = httpx2.Client()
>>> response = client.get("https://www.example.com/", auth=auth)
```
Or configured on the client instance, ensuring that all outgoing requests will include authentication credentials...
```pycon
>>> auth = httpx2.BasicAuth(username="username", password="secret")
>>> client = httpx2.Client(auth=auth)
>>> response = client.get("https://www.example.com/")
```
## Basic authentication
HTTP basic authentication is an unencrypted authentication scheme that uses a simple encoding of the username and password in the request `Authorization` header. Since it is unencrypted it should typically only be used over `https`, although this is not strictly enforced.
```pycon
>>> auth = httpx2.BasicAuth(username="finley", password="secret")
>>> client = httpx2.Client(auth=auth)
>>> response = client.get("https://httpbin.org/basic-auth/finley/secret")
>>> response
```
## Digest authentication
HTTP digest authentication is a challenge-response authentication scheme. Unlike basic authentication it provides encryption, and can be used over unencrypted `http` connections. It requires an additional round-trip in order to negotiate the authentication.
```pycon
>>> auth = httpx2.DigestAuth(username="olivia", password="secret")
>>> client = httpx2.Client(auth=auth)
>>> response = client.get("https://httpbin.org/digest-auth/auth/olivia/secret")
>>> response
>>> response.history
[]
```
## NetRC authentication
HTTPX can be configured to use [a `.netrc` config file](https://everything.curl.dev/usingcurl/netrc) for authentication.
The `.netrc` config file allows authentication credentials to be associated with specified hosts. When a request is made to a host that is found in the netrc file, the username and password will be included using HTTP basic authentication.
Example `.netrc` file:
```
machine example.org
login example-username
password example-password
machine python-httpx2.org
login other-username
password other-password
```
Some examples of configuring `.netrc` authentication with `httpx2`.
Use the default `.netrc` file in the users home directory:
```pycon
>>> auth = httpx2.NetRCAuth()
>>> client = httpx2.Client(auth=auth)
```
Use an explicit path to a `.netrc` file:
```pycon
>>> auth = httpx2.NetRCAuth(file="/path/to/.netrc")
>>> client = httpx2.Client(auth=auth)
```
Use the `NETRC` environment variable to configure a path to the `.netrc` file,
or fallback to the default.
```pycon
>>> auth = httpx2.NetRCAuth(file=os.environ.get("NETRC"))
>>> client = httpx2.Client(auth=auth)
```
The `NetRCAuth()` class uses [the `netrc.netrc()` function from the Python standard library](https://docs.python.org/3/library/netrc.html). See the documentation there for more details on exceptions that may be raised if the `.netrc` file is not found, or cannot be parsed.
## Custom authentication schemes
When issuing requests or instantiating a client, the `auth` argument can be used to pass an authentication scheme to use. The `auth` argument may be one of the following...
* A two-tuple of `username`/`password`, to be used with basic authentication.
* An instance of `httpx2.BasicAuth()`, `httpx2.DigestAuth()`, or `httpx2.NetRCAuth()`.
* A callable, accepting a request and returning an authenticated request instance.
* An instance of subclasses of `httpx2.Auth`.
The most involved of these is the last, which allows you to create authentication flows involving one or more requests. A subclass of `httpx2.Auth` should implement `def auth_flow(request)`, and yield any requests that need to be made...
```python
class MyCustomAuth(httpx2.Auth):
def __init__(self, token):
self.token = token
def auth_flow(self, request):
# Send the request, with a custom `X-Authentication` header.
request.headers['X-Authentication'] = self.token
yield request
```
If the auth flow requires more than one request, you can issue multiple yields, and obtain the response in each case...
```python
class MyCustomAuth(httpx2.Auth):
def __init__(self, token):
self.token = token
def auth_flow(self, request):
response = yield request
if response.status_code == 401:
# If the server issues a 401 response then resend the request,
# with a custom `X-Authentication` header.
request.headers['X-Authentication'] = self.token
yield request
```
Custom authentication classes are designed to not perform any I/O, so that they may be used with both sync and async client instances. If you are implementing an authentication scheme that requires the request body, then you need to indicate this on the class using a `requires_request_body` property.
You will then be able to access `request.content` inside the `.auth_flow()` method.
```python
class MyCustomAuth(httpx2.Auth):
requires_request_body = True
def __init__(self, token):
self.token = token
def auth_flow(self, request):
response = yield request
if response.status_code == 401:
# If the server issues a 401 response then resend the request,
# with a custom `X-Authentication` header.
request.headers['X-Authentication'] = self.sign_request(...)
yield request
def sign_request(self, request):
# Create a request signature, based on `request.method`, `request.url`,
# `request.headers`, and `request.content`.
...
```
Similarly, if you are implementing a scheme that requires access to the response body, then use the `requires_response_body` property. You will then be able to access response body properties and methods such as `response.content`, `response.text`, `response.json()`, etc.
```python
class MyCustomAuth(httpx2.Auth):
requires_response_body = True
def __init__(self, access_token, refresh_token, refresh_url):
self.access_token = access_token
self.refresh_token = refresh_token
self.refresh_url = refresh_url
def auth_flow(self, request):
request.headers["X-Authentication"] = self.access_token
response = yield request
if response.status_code == 401:
# If the server issues a 401 response, then issue a request to
# refresh tokens, and resend the request.
refresh_response = yield self.build_refresh_request()
self.update_tokens(refresh_response)
request.headers["X-Authentication"] = self.access_token
yield request
def build_refresh_request(self):
# Return an `httpx2.Request` for refreshing tokens.
...
def update_tokens(self, response):
# Update the `.access_token` and `.refresh_token` tokens
# based on a refresh response.
data = response.json()
...
```
If you _do_ need to perform I/O other than HTTP requests, such as accessing a disk-based cache, or you need to use concurrency primitives, such as locks, then you should override `.sync_auth_flow()` and `.async_auth_flow()` (instead of `.auth_flow()`). The former will be used by `httpx2.Client`, while the latter will be used by `httpx2.AsyncClient`.
```python
import asyncio
import threading
import httpx2
class MyCustomAuth(httpx2.Auth):
def __init__(self):
self._sync_lock = threading.RLock()
self._async_lock = asyncio.Lock()
def sync_get_token(self):
with self._sync_lock:
...
def sync_auth_flow(self, request):
token = self.sync_get_token()
request.headers["Authorization"] = f"Token {token}"
yield request
async def async_get_token(self):
async with self._async_lock:
...
async def async_auth_flow(self, request):
token = await self.async_get_token()
request.headers["Authorization"] = f"Token {token}"
yield request
```
If you only want to support one of the two methods, then you should still override it, but raise an explicit `RuntimeError`.
```python
import httpx2
import sync_only_library
class MyCustomAuth(httpx2.Auth):
def sync_auth_flow(self, request):
token = sync_only_library.get_token(...)
request.headers["Authorization"] = f"Token {token}"
yield request
async def async_auth_flow(self, request):
raise RuntimeError("Cannot use a sync authentication class with httpx2.AsyncClient")
``` pydantic-httpx2-6dd59b9/docs/advanced/clients.md 0000664 0000000 0000000 00000026362 15265474634 0021715 0 ustar 00root root 0000000 0000000 !!! hint
If you are coming from Requests, `httpx2.Client()` is what you can use instead of `requests.Session()`.
## Why use a Client?
!!! note "TL;DR"
If you do anything more than experimentation, one-off scripts, or prototypes, then you should use a `Client` instance.
**More efficient usage of network resources**
When you make requests using the top-level API as documented in the [Quickstart](../quickstart.md) guide, HTTPX has to establish a new connection _for every single request_ (connections are not reused). As the number of requests to a host increases, this quickly becomes inefficient.
On the other hand, a `Client` instance uses [HTTP connection pooling](https://en.wikipedia.org/wiki/HTTP_persistent_connection). This means that when you make several requests to the same host, the `Client` will reuse the underlying TCP connection, instead of recreating one for every single request.
This can bring **significant performance improvements** compared to using the top-level API, including:
- Reduced latency across requests (no handshaking).
- Reduced CPU usage and round-trips.
- Reduced network congestion.
**Extra features**
`Client` instances also support features that aren't available at the top-level API, such as:
- Cookie persistence across requests.
- Applying configuration across all outgoing requests.
- Sending requests through HTTP proxies.
- Using [HTTP/2](../http2.md).
The other sections on this page go into further detail about what you can do with a `Client` instance.
## Usage
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 httpx2.Client() as client:
...
```
Alternatively, you can explicitly close the connection pool without block-usage using `.close()`:
```python
client = httpx2.Client()
try:
...
finally:
client.close()
```
## Making requests
Once you have a `Client`, you can send requests using `.get()`, `.post()`, etc. For example:
```pycon
>>> with httpx2.Client() as client:
... r = client.get('https://example.com')
...
>>> r
```
These methods accept the same arguments as `httpx2.get()`, `httpx2.post()`, etc. This means that all features documented in the [Quickstart](../quickstart.md) guide are also available at the client level.
For example, to send a request with custom headers:
```pycon
>>> with httpx2.Client() as client:
... headers = {'X-Custom': 'value'}
... r = client.get('https://example.com', headers=headers)
...
>>> r.request.headers['X-Custom']
'value'
```
## Sharing configuration across requests
Clients allow you to apply configuration to all outgoing requests by passing parameters to the `Client` constructor.
For example, to apply a set of custom headers _on every request_:
```pycon
>>> url = 'http://httpbin.org/headers'
>>> headers = {'user-agent': 'my-app/0.0.1'}
>>> with httpx2.Client(headers=headers) as client:
... r = client.get(url)
...
>>> r.json()['headers']['User-Agent']
'my-app/0.0.1'
```
## Merging of configuration
When a configuration option is provided at both the client-level and request-level, one of two things can happen:
- For headers, query parameters and cookies, the values are combined together. For example:
```pycon
>>> headers = {'X-Auth': 'from-client'}
>>> params = {'client_id': 'client1'}
>>> with httpx2.Client(headers=headers, params=params) as client:
... headers = {'X-Custom': 'from-request'}
... params = {'request_id': 'request1'}
... r = client.get('https://example.com', headers=headers, params=params)
...
>>> r.request.url
URL('https://example.com?client_id=client1&request_id=request1')
>>> r.request.headers['X-Auth']
'from-client'
>>> r.request.headers['X-Custom']
'from-request'
```
- For all other parameters, the request-level value takes priority. For example:
```pycon
>>> with httpx2.Client(auth=('tom', 'mot123')) as client:
... r = client.get('https://example.com', auth=('alice', 'ecila123'))
...
>>> _, _, auth = r.request.headers['Authorization'].partition(' ')
>>> import base64
>>> base64.b64decode(auth)
b'alice:ecila123'
```
If you need finer-grained control on the merging of client-level and request-level parameters, see [Request instances](#request-instances).
## Other Client-only configuration options
Additionally, `Client` accepts some configuration options that aren't available at the request level.
For example, `base_url` allows you to prepend an URL to all outgoing requests:
```pycon
>>> with httpx2.Client(base_url='http://httpbin.org') as client:
... r = client.get('/headers')
...
>>> r.request.url
URL('http://httpbin.org/headers')
```
For a list of all available client parameters, see the [`Client`](../api.md#client) API reference.
---
## Request instances
For maximum control on what gets sent over the wire, HTTPX supports building explicit [`Request`](../api.md#request) instances:
```python
request = httpx2.Request("GET", "https://example.com")
```
To dispatch a `Request` instance across to the network, create a [`Client` instance](#sharing-configuration-across-requests) and use `.send()`:
```python
with httpx2.Client() as client:
response = client.send(request)
...
```
If you need to mix client-level and request-level options in a way that is not supported by the default [Merging of configuration](#merging-of-configuration), you can use `.build_request()` and then make arbitrary modifications to the `Request` instance. For example:
```python
headers = {"X-Api-Key": "...", "X-Client-ID": "ABC123"}
with httpx2.Client(headers=headers) as client:
request = client.build_request("GET", "https://api.example.com")
print(request.headers["X-Client-ID"]) # "ABC123"
# Don't send the API key for this particular request.
del request.headers["X-Api-Key"]
response = client.send(request)
...
```
## Monitoring download progress
If you need to monitor download progress of large responses, you can use response streaming and inspect the `response.num_bytes_downloaded` property.
This interface is required for properly determining download progress, because the total number of bytes returned by `response.content` or `response.iter_content()` will not always correspond with the raw content length of the response if HTTP response compression is being used.
For example, showing a progress bar using the [`tqdm`](https://github.com/tqdm/tqdm) library while a response is being downloaded could be done like thisโฆ
```python
import tempfile
import httpx2
from tqdm import tqdm
with tempfile.NamedTemporaryFile() as download_file:
url = "https://speed.hetzner.de/100MB.bin"
with httpx2.stream("GET", url) as response:
total = int(response.headers["Content-Length"])
with tqdm(total=total, unit_scale=True, unit_divisor=1024, unit="B") as progress:
num_bytes_downloaded = response.num_bytes_downloaded
for chunk in response.iter_bytes():
download_file.write(chunk)
progress.update(response.num_bytes_downloaded - num_bytes_downloaded)
num_bytes_downloaded = response.num_bytes_downloaded
```

Or an alternate example, this time using the [`rich`](https://github.com/willmcgugan/rich) libraryโฆ
```python
import tempfile
import httpx2
import rich.progress
with tempfile.NamedTemporaryFile() as download_file:
url = "https://speed.hetzner.de/100MB.bin"
with httpx2.stream("GET", url) as response:
total = int(response.headers["Content-Length"])
with rich.progress.Progress(
"[progress.percentage]{task.percentage:>3.0f}%",
rich.progress.BarColumn(bar_width=None),
rich.progress.DownloadColumn(),
rich.progress.TransferSpeedColumn(),
) as progress:
download_task = progress.add_task("Download", total=total)
for chunk in response.iter_bytes():
download_file.write(chunk)
progress.update(download_task, completed=response.num_bytes_downloaded)
```

## Monitoring upload progress
If you need to monitor upload progress of large responses, you can use request content generator streaming.
For example, showing a progress bar using the [`tqdm`](https://github.com/tqdm/tqdm) library.
```python
import io
import random
import httpx2
from tqdm import tqdm
def gen():
"""
this is a complete example with generated random bytes.
you can replace `io.BytesIO` with real file object.
"""
total = 32 * 1024 * 1024 # 32m
with tqdm(ascii=True, unit_scale=True, unit='B', unit_divisor=1024, total=total) as bar:
with io.BytesIO(random.randbytes(total)) as f:
while data := f.read(1024):
yield data
bar.update(len(data))
httpx2.post("https://httpbin.org/post", content=gen())
```

## Multipart file encoding
As mentioned in the [quickstart](../quickstart.md#sending-multipart-file-uploads)
multipart file encoding is available by passing a dictionary with the
name of the payloads as keys and either tuple of elements or a file-like object or a string as values.
```pycon
>>> with open('report.xls', 'rb') as report_file:
... files = {'upload-file': ('report.xls', report_file, 'application/vnd.ms-excel')}
... r = httpx2.post("https://httpbin.org/post", files=files)
>>> print(r.text)
{
...
"files": {
"upload-file": "<... binary content ...>"
},
...
}
```
More specifically, if a tuple is used as a value, it must have between 2 and 3 elements:
- The first element is an optional file name which can be set to `None`.
- The second element may be a file-like object or a string which will be automatically
encoded in UTF-8.
- An optional third element can be used to specify the
[MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_Types)
of the file being uploaded. If not specified HTTPX will attempt to guess the MIME type based
on the file name, with unknown file extensions defaulting to "application/octet-stream".
If the file name is explicitly set to `None` then HTTPX will not include a content-type
MIME header field.
```pycon
>>> files = {'upload-file': (None, 'text content', 'text/plain')}
>>> r = httpx2.post("https://httpbin.org/post", files=files)
>>> print(r.text)
{
...
"files": {},
"form": {
"upload-file": "text-content"
},
...
}
```
!!! tip
It is safe to upload large files this way. File uploads are streaming by default, meaning that only one chunk will be loaded into memory at a time.
Non-file data fields can be included in the multipart form using by passing them to `data=...`.
You can also send multiple files in one go with a multiple file field form.
To do that, pass a list of `(field, )` items instead of a dictionary, allowing you to pass multiple items with the same `field`.
For instance this request sends 2 files, `foo.png` and `bar.png` in one request on the `images` form field:
```pycon
>>> with open('foo.png', 'rb') as foo_file, open('bar.png', 'rb') as bar_file:
... files = [
... ('images', ('foo.png', foo_file, 'image/png')),
... ('images', ('bar.png', bar_file, 'image/png')),
... ]
... r = httpx2.post("https://httpbin.org/post", files=files)
```
pydantic-httpx2-6dd59b9/docs/advanced/emscripten.md 0000664 0000000 0000000 00000004661 15265474634 0022423 0 ustar 00root root 0000000 0000000 ---
template: pyodide.html
---
# Emscripten Support
httpx2 has support for running on WebAssembly / Emscripten using
[Pyodide](https://github.com/pyodide/pyodide/) or
[emscripten-forge](https://emscripten-forge.org/). It uses the `httpx2-jsfetch`
package ([for Pyodide](https://github.com/hoodmane/httpx2-jsfetch) or
[for emscripten-forge](https://github.com/davidbrochart/httpx2-jsfetch-emscripten-forge))
to define the `JavascriptFetchTransport` and `AsyncJavascriptFetchTransport`.
Asynchronous requests always use `fetch`. Synchronous requests use the following
methods:
1. If [JavaScript Promise Integration](https://github.com/WebAssembly/js-promise-integration/blob/main/proposals/js-promise-integration/Overview.md)
(JSPI) is supported by the JavaScript runtime, the request will be made with
`fetch` and stack switching.
2. Otherwise, if in a browser, the request will be made using a synchronous
`XMLHttpRequest`.
3. Otherwise, if in Node, the request will fail. Synchronous requests in Node
require JSPI.
In Emscripten, all network connections are handled by the enclosing JavaScript
runtime. As such, there is limited control over various features. In particular:
- Proxy servers are handled by the runtime, so httpx2 cannot control them.
- httpx2 has no control over connection pooling.
- Certificate handling is done by the browser, so httpx2 cannot modify it.
- Requests are constrained by cross-origin isolation settings in the same way as
any request that is originated by JavaScript code.
- Timeouts will not work in the main browser thread unless the browser supports
JSPI because main thread synchronous `XMLHttpRequest` does not support
timeouts.
Setting any of the transport options that depend on these features (`verify`,
`cert`, `http2`, `limits`, `proxy`, `uds`, `local_address`, `retries`, or
`socket_options`) will emit a `UserWarning` and the option will be silently
ignored.
## Try it in your browser
Use the following live example to test httpx2 in your web browser. You can
change the code below and hit run again to test different features or web
addresses.
import httpx2
from js import document
print("Sending response using httpx2 in the browser:")
print("--------------------------------------------")
r = httpx2.get(document.location.origin)
print("Status = ", r.status_code)
print("Response = ", r.text[:50], "...")
pydantic-httpx2-6dd59b9/docs/advanced/event-hooks.md 0000664 0000000 0000000 00000004571 15265474634 0022514 0 ustar 00root root 0000000 0000000 HTTPX allows you to register "event hooks" with the client, that are called
every time a particular type of event takes place.
There are currently two event hooks:
* `request` - Called after a request is fully prepared, but before it is sent to the network. Passed the `request` instance.
* `response` - Called after the response has been fetched from the network, but before it is returned to the caller. Passed the `response` instance.
These allow you to install client-wide functionality such as logging, monitoring or tracing.
```python
def log_request(request):
print(f"Request event hook: {request.method} {request.url} - Waiting for response")
def log_response(response):
request = response.request
print(f"Response event hook: {request.method} {request.url} - Status {response.status_code}")
client = httpx2.Client(event_hooks={'request': [log_request], 'response': [log_response]})
```
You can also use these hooks to install response processing code, such as this
example, which creates a client instance that always raises `httpx2.HTTPStatusError`
on 4xx and 5xx responses.
```python
def raise_on_4xx_5xx(response):
response.raise_for_status()
client = httpx2.Client(event_hooks={'response': [raise_on_4xx_5xx]})
```
!!! note
Response event hooks are called before determining if the response body
should be read or not.
If you need access to the response body inside an event hook, you'll
need to call `response.read()`, or for AsyncClients, `response.aread()`.
The hooks are also allowed to modify `request` and `response` objects.
```python
def add_timestamp(request):
request.headers['x-request-timestamp'] = datetime.now(tz=datetime.utc).isoformat()
client = httpx2.Client(event_hooks={'request': [add_timestamp]})
```
Event hooks must always be set as a **list of callables**, and you may register
multiple event hooks for each type of event.
As well as being able to set event hooks on instantiating the client, there
is also an `.event_hooks` property, that allows you to inspect and modify
the installed hooks.
```python
client = httpx2.Client()
client.event_hooks['request'] = [log_request]
client.event_hooks['response'] = [log_response, raise_on_4xx_5xx]
```
!!! note
If you are using HTTPX's async support, then you need to be aware that
hooks registered with `httpx2.AsyncClient` MUST be async functions,
rather than plain functions.
pydantic-httpx2-6dd59b9/docs/advanced/extensions.md 0000664 0000000 0000000 00000022500 15265474634 0022441 0 ustar 00root root 0000000 0000000 # Extensions
Request and response extensions provide a untyped space where additional information may be added.
Extensions should be used for features that may not be available on all transports, and that do not fit neatly into [the simplified request/response model](https://www.encode.io/httpcore/extensions/) that the underlying `httpcore2` package inherits from upstream `httpcore` and uses as its API.
Several extensions are supported on the request:
```python
# Request timeouts actually implemented as an extension on
# the request, ensuring that they are passed throughout the
# entire call stack.
client = httpx2.Client()
response = client.get(
"https://www.example.com",
extensions={"timeout": {"connect": 5.0}}
)
response.request.extensions["timeout"]
{"connect": 5.0}
```
And on the response:
```python
client = httpx2.Client()
response = client.get("https://www.example.com")
print(response.extensions["http_version"]) # b"HTTP/1.1"
# Other server responses could have been
# b"HTTP/0.9", b"HTTP/1.0", or b"HTTP/1.1"
```
## Request Extensions
### `"trace"`
The trace extension allows a callback handler to be installed to monitor the internal
flow of events within the underlying `httpcore2` transport.
The simplest way to explain this is with an example:
```python
import httpx2
def log(event_name, info):
print(event_name, info)
client = httpx2.Client()
response = client.get("https://www.example.com/", extensions={"trace": log})
# connection.connect_tcp.started {'host': 'www.example.com', 'port': 443, 'local_address': None, 'timeout': 5.0, 'socket_options': None}
# connection.connect_tcp.complete {'return_value': }
# connection.start_tls.started {'ssl_context': , 'server_hostname': 'www.example.com', 'timeout': 5.0}
# connection.start_tls.complete {'return_value': }
# http11.send_request_headers.started {'request': }
# http11.send_request_headers.complete {'return_value': None}
# http11.send_request_body.started {'request': }
# http11.send_request_body.complete {'return_value': None}
# http11.receive_response_headers.started {'request': }
# http11.receive_response_headers.complete {'return_value': (b'HTTP/1.1', 200, b'OK', [(b'Age', b'553715'), (b'Cache-Control', b'max-age=604800'), (b'Content-Type', b'text/html; charset=UTF-8'), (b'Date', b'Thu, 21 Oct 2021 17:08:42 GMT'), (b'Etag', b'"3147526947+ident"'), (b'Expires', b'Thu, 28 Oct 2021 17:08:42 GMT'), (b'Last-Modified', b'Thu, 17 Oct 2019 07:18:26 GMT'), (b'Server', b'ECS (nyb/1DCD)'), (b'Vary', b'Accept-Encoding'), (b'X-Cache', b'HIT'), (b'Content-Length', b'1256')])}
# http11.receive_response_body.started {'request': }
# http11.receive_response_body.complete {'return_value': None}
# http11.response_closed.started {}
# http11.response_closed.complete {'return_value': None}
```
The `event_name` and `info` arguments here will be one of the following:
* `{event_type}.{event_name}.started`, ``
* `{event_type}.{event_name}.complete`, `{"return_value": <...>}`
* `{event_type}.{event_name}.failed`, `{"exception": <...>}`
Note that when using async code the handler function passed to `"trace"` must be an `async def ...` function.
The following event types are currently exposed...
**Establishing the connection**
* `"connection.connect_tcp"`
* `"connection.connect_unix_socket"`
* `"connection.start_tls"`
**HTTP/1.1 events**
* `"http11.send_request_headers"`
* `"http11.send_request_body"`
* `"http11.receive_response"`
* `"http11.receive_response_body"`
* `"http11.response_closed"`
**HTTP/2 events**
* `"http2.send_connection_init"`
* `"http2.send_request_headers"`
* `"http2.send_request_body"`
* `"http2.receive_response_headers"`
* `"http2.receive_response_body"`
* `"http2.response_closed"`
The exact set of trace events may be subject to change across different versions of `httpcore2`. If you need to rely on a particular set of events it is recommended that you pin installation of the package to a fixed version.
### `"sni_hostname"`
The server's hostname, which is used to confirm the hostname supplied by the SSL certificate.
If you want to connect to an explicit IP address rather than using the standard DNS hostname lookup, then you'll need to use this request extension.
For example:
``` python
# Connect to '185.199.108.153' but use 'www.encode.io' in the Host header,
#ย and use 'www.encode.io' when SSL verifying the server hostname.
client = httpx2.Client()
headers = {"Host": "www.encode.io"}
extensions = {"sni_hostname": "www.encode.io"}
response = client.get(
"https://185.199.108.153/path",
headers=headers,
extensions=extensions
)
```
### `"timeout"`
A dictionary of `str: Optional[float]` timeout values.
May include values for `'connect'`, `'read'`, `'write'`, or `'pool'`.
For example:
```python
# Timeout if a connection takes more than 5 seconds to established, or if
# we are blocked waiting on the connection pool for more than 10 seconds.
client = httpx2.Client()
response = client.get(
"https://www.example.com",
extensions={"timeout": {"connect": 5.0, "pool": 10.0}}
)
```
This extension is how the `httpx2` timeouts are implemented, ensuring that the timeout values are associated with the request instance and passed throughout the stack. You shouldn't typically be working with this extension directly, but use the higher level `timeout` API instead.
### `"target"`
The target that is used as [the HTTP target instead of the URL path](https://datatracker.ietf.org/doc/html/rfc2616#section-5.1.2).
This enables support constructing requests that would otherwise be unsupported.
* URL paths with non-standard escaping applied.
* Forward proxy requests using an absolute URI.
* Tunneling proxy requests using `CONNECT` with hostname as the target.
* Server-wide `OPTIONS *` requests.
Some examples:
Using the 'target' extension to send requests without the standard path escaping rules...
```python
# Typically a request to "https://www.example.com/test^path" would
# connect to "www.example.com" and send an HTTP/1.1 request like...
#
# GET /test%5Epath HTTP/1.1
#
# Using the target extension we can include the literal '^'...
#
# GET /test^path HTTP/1.1
#
# Note that requests must still be valid HTTP requests.
# For example including whitespace in the target will raise a `LocalProtocolError`.
extensions = {"target": b"/test^path"}
response = httpx2.get("https://www.example.com", extensions=extensions)
```
The `target` extension also allows server-wide `OPTIONS *` requests to be constructed...
```python
# This will send the following request...
#
# CONNECT * HTTP/1.1
extensions = {"target": b"*"}
response = httpx2.request("CONNECT", "https://www.example.com", extensions=extensions)
```
## Response Extensions
### `"http_version"`
The HTTP version, as bytes. Eg. `b"HTTP/1.1"`.
When using HTTP/1.1 the response line includes an explicit version, and the value of this key could feasibly be one of `b"HTTP/0.9"`, `b"HTTP/1.0"`, or `b"HTTP/1.1"`.
When using HTTP/2 there is no further response versioning included in the protocol, and the value of this key will always be `b"HTTP/2"`.
### `"reason_phrase"`
The reason-phrase of the HTTP response, as bytes. For example `b"OK"`. Some servers may include a custom reason phrase, although this is not recommended.
HTTP/2 onwards does not include a reason phrase on the wire.
When no key is included, a default based on the status code may be used.
### `"stream_id"`
When HTTP/2 is being used the `"stream_id"` response extension can be accessed to determine the ID of the data stream that the response was sent on.
### `"network_stream"`
The `"network_stream"` extension allows developers to handle HTTP `CONNECT` and `Upgrade` requests, by providing an API that steps outside the standard request/response model, and can directly read or write to the network.
The interface provided by the network stream:
* `read(max_bytes, timeout = None) -> bytes`
* `write(buffer, timeout = None)`
* `close()`
* `start_tls(ssl_context, server_hostname = None, timeout = None) -> NetworkStream`
* `get_extra_info(info) -> Any`
This API can be used as the foundation for working with HTTP proxies, WebSocket upgrades, and other advanced use-cases.
See the [network backends documentation](https://www.encode.io/httpcore/network-backends/) for more information on working directly with network streams.
**Extra network information**
The network stream abstraction also allows access to various low-level information that may be exposed by the underlying socket:
```python
response = httpx2.get("https://www.example.com")
network_stream = response.extensions["network_stream"]
client_addr = network_stream.get_extra_info("client_addr")
server_addr = network_stream.get_extra_info("server_addr")
print("Client address", client_addr)
print("Server address", server_addr)
```
The socket SSL information is also available through this interface, although you need to ensure that the underlying connection is still open, in order to access it...
```python
with httpx2.stream("GET", "https://www.example.com") as response:
network_stream = response.extensions["network_stream"]
ssl_object = network_stream.get_extra_info("ssl_object")
print("TLS version", ssl_object.version())
```
pydantic-httpx2-6dd59b9/docs/advanced/proxies.md 0000664 0000000 0000000 00000007020 15265474634 0021733 0 ustar 00root root 0000000 0000000 HTTPX supports setting up [HTTP proxies](https://en.wikipedia.org/wiki/Proxy_server#Web_proxy_servers) via the `proxy` parameter to be passed on client initialization or top-level API functions like `httpx2.get(..., proxy=...)`.
Diagram of how a proxy works (source: Wikipedia). The left hand side "Internet" blob may be your HTTPX client requesting example.com through a proxy.
## HTTP Proxies
To route all traffic (HTTP and HTTPS) to a proxy located at `http://localhost:8030`, pass the proxy URL to the client...
```python
with httpx2.Client(proxy="http://localhost:8030") as client:
...
```
For more advanced use cases, pass a mounts `dict`. For example, to route HTTP and HTTPS requests to 2 different proxies, respectively located at `http://localhost:8030`, and `http://localhost:8031`, pass a `dict` of proxy URLs:
```python
proxy_mounts = {
"http://": httpx2.HTTPTransport(proxy="http://localhost:8030"),
"https://": httpx2.HTTPTransport(proxy="http://localhost:8031"),
}
with httpx2.Client(mounts=proxy_mounts) as client:
...
```
For detailed information about how `mounts` routes requests, see the [Routing](transports.md#routing) section.
!!! tip "Gotcha"
In most cases, the proxy URL for the `https://` key _should_ use the `http://` scheme (that's not a typo!).
This is because HTTP proxying requires initiating a connection with the proxy server. While it's possible that your proxy supports doing it via HTTPS, most proxies only support doing it via HTTP.
For more information, see [FORWARD vs TUNNEL](#forward-vs-tunnel).
## Authentication
Proxy credentials can be passed as the `userinfo` section of the proxy URL. For example:
```python
with httpx2.Client(proxy="http://username:password@localhost:8030") as client:
...
```
## Proxy mechanisms
!!! note
This section describes **advanced** proxy concepts and functionality.
### FORWARD vs TUNNEL
In general, the flow for making an HTTP request through a proxy is as follows:
1. The client connects to the proxy (initial connection request).
2. The proxy transfers data to the server on your behalf.
How exactly step 2/ is performed depends on which of two proxying mechanisms is used:
* **Forwarding**: the proxy makes the request for you, and sends back the response it obtained from the server.
* **Tunnelling**: the proxy establishes a TCP connection to the server on your behalf, and the client reuses this connection to send the request and receive the response. This is known as an [HTTP Tunnel](https://en.wikipedia.org/wiki/HTTP_tunnel). This mechanism is how you can access websites that use HTTPS from an HTTP proxy (the client "upgrades" the connection to HTTPS by performing the TLS handshake with the server over the TCP connection provided by the proxy).
### Troubleshooting proxies
If you encounter issues when setting up proxies, please refer to our [Troubleshooting guide](../troubleshooting.md#proxies).
## SOCKS
In addition to HTTP proxies, `httpcore2` also supports proxies using the SOCKS protocol.
This is an optional feature that requires an additional third-party library be installed before use.
You can install SOCKS support using `pip`:
```shell
pip install 'httpx2[socks]'
```
You can now configure a client to make requests via a proxy using the SOCKS protocol:
```python
httpx2.Client(proxy='socks5://user:pass@host:port')
```
pydantic-httpx2-6dd59b9/docs/advanced/resource-limits.md 0000664 0000000 0000000 00000001127 15265474634 0023372 0 ustar 00root root 0000000 0000000 You can control the connection pool size using the `limits` keyword
argument on the client. It takes instances of `httpx2.Limits` which define:
- `max_keepalive_connections`, number of allowable keep-alive connections, or `None` to always
allow. (Defaults 20)
- `max_connections`, maximum number of allowable connections, or `None` for no limits.
(Default 100)
- `keepalive_expiry`, time limit on idle keep-alive connections in seconds, or `None` for no limits. (Default 5)
```python
limits = httpx2.Limits(max_keepalive_connections=5, max_connections=10)
client = httpx2.Client(limits=limits)
``` pydantic-httpx2-6dd59b9/docs/advanced/ssl.md 0000664 0000000 0000000 00000007131 15265474634 0021046 0 ustar 00root root 0000000 0000000 When making a request over HTTPS, HTTPX needs to verify the identity of the requested host. To do this, it uses a bundle of SSL certificates (a.k.a. CA bundle) delivered by a trusted certificate authority (CA).
### Enabling and disabling verification
By default httpx2 will verify HTTPS connections, and raise an error for invalid SSL cases...
```pycon
>>> httpx2.get("https://expired.badssl.com/")
httpx2.ConnectError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired (_ssl.c:997)
```
You can disable SSL verification completely and allow insecure requests...
```pycon
>>> httpx2.get("https://expired.badssl.com/", verify=False)
```
### Configuring client instances
If you're using a `Client()` instance you should pass any `verify=<...>` configuration when instantiating the client.
By default the operating system's trust store is used for SSL verification, via [the `truststore` package](https://truststore.readthedocs.io/).
For more complex configurations you can pass an [SSL Context](https://docs.python.org/3/library/ssl.html) instance...
```python
import ssl
import truststore
import httpx2
# This SSL context is equivalent to the default `verify=True`.
ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
client = httpx2.Client(verify=ctx)
```
Using [the `certifi` CA bundle](https://certifiio.readthedocs.io/en/latest/) instead of the system trust store...
```python
import certifi
import httpx2
import ssl
# Use the certifi CA bundle.
ctx = ssl.create_default_context(cafile=certifi.where())
client = httpx2.Client(verify=ctx)
```
Loading an alternative certificate verification store using [the standard SSL context API](https://docs.python.org/3/library/ssl.html)...
```python
import httpx2
import ssl
# Use an explicitly configured certificate store.
ctx = ssl.create_default_context(cafile="path/to/certs.pem") # Either cafile or capath.
client = httpx2.Client(verify=ctx)
```
### Client side certificates
Client side certificates allow a remote server to verify the client. They tend to be used within private organizations to authenticate requests to remote servers.
You can specify client-side certificates, using the [`.load_cert_chain()`](https://docs.python.org/3/library/ssl.html#ssl.SSLContext.load_cert_chain) API...
```python
ctx = ssl.create_default_context()
ctx.load_cert_chain(certfile="path/to/client.pem") # Optionally also keyfile or password.
client = httpx2.Client(verify=ctx)
```
### Working with `SSL_CERT_FILE` and `SSL_CERT_DIR`
`httpx2` does respect the `SSL_CERT_FILE` and `SSL_CERT_DIR` environment variables by default. For details, refer to [the section on the environment variables page](../environment_variables.md#ssl_cert_file).
### Making HTTPS requests to a local server
When making requests to local servers, such as a development server running on `localhost`, you will typically be using unencrypted HTTP connections.
If you do need to make HTTPS connections to a local server, for example to test an HTTPS-only service, you will need to create and use your own certificates. Here's one way to do it...
1. Use [trustme](https://github.com/python-trio/trustme) to generate a pair of server key/cert files, and a client cert file.
2. Pass the server key/cert files when starting your local server. (This depends on the particular web server you're using. For example, [Uvicorn](https://www.uvicorn.org) provides the `--ssl-keyfile` and `--ssl-certfile` options.)
3. Configure `httpx2` to use the certificates stored in `client.pem`.
```python
ctx = ssl.create_default_context(cafile="client.pem")
client = httpx2.Client(verify=ctx)
```
pydantic-httpx2-6dd59b9/docs/advanced/text-encodings.md 0000664 0000000 0000000 00000007113 15265474634 0023200 0 ustar 00root root 0000000 0000000 When accessing `response.text`, we need to decode the response bytes into a unicode text representation.
By default `httpx2` will use `"charset"` information included in the response `Content-Type` header to determine how the response bytes should be decoded into text.
In cases where no charset information is included on the response, the default behaviour is to assume "utf-8" encoding, which is by far the most widely used text encoding on the internet.
## Using the default encoding
To understand this better let's start by looking at the default behaviour for text decoding...
```python
import httpx2
# Instantiate a client with the default configuration.
client = httpx2.Client()
# Using the client...
response = client.get(...)
print(response.encoding) # This will either print the charset given in
# the Content-Type charset, or else "utf-8".
print(response.text) # The text will either be decoded with the Content-Type
# charset, or using "utf-8".
```
This is normally absolutely fine. Most servers will respond with a properly formatted Content-Type header, including a charset encoding. And in most cases where no charset encoding is included, UTF-8 is very likely to be used, since it is so widely adopted.
## Using an explicit encoding
In some cases we might be making requests to a site where no character set information is being set explicitly by the server, but we know what the encoding is. In this case it's best to set the default encoding explicitly on the client.
```python
import httpx2
# Instantiate a client with a Japanese character set as the default encoding.
client = httpx2.Client(default_encoding="shift-jis")
# Using the client...
response = client.get(...)
print(response.encoding) # This will either print the charset given in
# the Content-Type charset, or else "shift-jis".
print(response.text) # The text will either be decoded with the Content-Type
# charset, or using "shift-jis".
```
## Using auto-detection
In cases where the server is not reliably including character set information, and where we don't know what encoding is being used, we can enable auto-detection to make a best-guess attempt when decoding from bytes to text.
To use auto-detection you need to set the `default_encoding` argument to a callable instead of a string. This callable should be a function which takes the input bytes as an argument and returns the character set to use for decoding those bytes to text.
There are two widely used Python packages which both handle this functionality:
* [`chardet`](https://chardet.readthedocs.io/) - This is a well established package, and is a port of [the auto-detection code in Mozilla](https://www-archive.mozilla.org/projects/intl/chardet.html).
* [`charset-normalizer`](https://charset-normalizer.readthedocs.io/) - A newer package, motivated by `chardet`, with a different approach.
Let's take a look at installing autodetection using one of these packages...
```shell
pip install httpx2
pip install chardet
```
Once `chardet` is installed, we can configure a client to use character-set autodetection.
```python
import httpx2
import chardet
def autodetect(content):
return chardet.detect(content).get("encoding")
# Using a client with character-set autodetection enabled.
client = httpx2.Client(default_encoding=autodetect)
response = client.get(...)
print(response.encoding) # This will either print the charset given in
# the Content-Type charset, or else the auto-detected
# character set.
print(response.text)
```
pydantic-httpx2-6dd59b9/docs/advanced/timeouts.md 0000664 0000000 0000000 00000005324 15265474634 0022120 0 ustar 00root root 0000000 0000000 HTTPX is careful to enforce timeouts everywhere by default.
The default behavior is to raise a `TimeoutException` after 5 seconds of
network inactivity.
## Setting and disabling timeouts
You can set timeouts for an individual request:
```python
# Using the top-level API:
httpx2.get('http://example.com/api/v1/example', timeout=10.0)
# Using a client instance:
with httpx2.Client() as client:
client.get("http://example.com/api/v1/example", timeout=10.0)
```
Or disable timeouts for an individual request:
```python
# Using the top-level API:
httpx2.get('http://example.com/api/v1/example', timeout=None)
# Using a client instance:
with httpx2.Client() as client:
client.get("http://example.com/api/v1/example", timeout=None)
```
## Setting a default timeout on a client
You can set a timeout on a client instance, which results in the given
`timeout` being used as the default for requests made with this client:
```python
client = httpx2.Client() # Use a default 5s timeout everywhere.
client = httpx2.Client(timeout=10.0) # Use a default 10s timeout everywhere.
client = httpx2.Client(timeout=None) # Disable all timeouts by default.
```
## Fine tuning the configuration
HTTPX also allows you to specify the timeout behavior in more fine grained detail.
There are four different types of timeouts that may occur. These are **connect**,
**read**, **write**, and **pool** timeouts.
* The **connect** timeout specifies the maximum amount of time to wait until
a socket connection to the requested host is established. If HTTPX is unable to connect
within this time frame, a `ConnectTimeout` exception is raised.
* The **read** timeout specifies the maximum duration to wait for a chunk of
data to be received (for example, a chunk of the response body). If HTTPX is
unable to receive data within this time frame, a `ReadTimeout` exception is raised.
* The **write** timeout specifies the maximum duration to wait for a chunk of
data to be sent (for example, a chunk of the request body). If HTTPX is unable
to send data within this time frame, a `WriteTimeout` exception is raised.
* The **pool** timeout specifies the maximum duration to wait for acquiring
a connection from the connection pool. If HTTPX is unable to acquire a connection
within this time frame, a `PoolTimeout` exception is raised. A related
configuration here is the maximum number of allowable connections in the
connection pool, which is configured by the `limits` argument.
You can configure the timeout behavior for any of these values...
```python
# A client with a 60s timeout for connecting, and a 10s timeout elsewhere.
timeout = httpx2.Timeout(10.0, connect=60.0)
client = httpx2.Client(timeout=timeout)
response = client.get('http://example.com/')
``` pydantic-httpx2-6dd59b9/docs/advanced/transports.md 0000664 0000000 0000000 00000033620 15265474634 0022466 0 ustar 00root root 0000000 0000000 HTTPX's `Client` also accepts a `transport` argument. This argument allows you
to provide a custom Transport object that will be used to perform the actual
sending of the requests.
## HTTP Transport
For some advanced configuration you might need to instantiate a transport
class directly, and pass it to the client instance. One example is the
`local_address` configuration which is only available via this low-level API.
```pycon
>>> import httpx2
>>> transport = httpx2.HTTPTransport(local_address="0.0.0.0")
>>> client = httpx2.Client(transport=transport)
```
Connection retries are also available via this interface. Requests will be retried the given number of times in case an `httpx2.ConnectError` or an `httpx2.ConnectTimeout` occurs, allowing smoother operation under flaky networks. If you need other forms of retry behaviors, such as handling read/write errors or reacting to `503 Service Unavailable`, consider general-purpose tools such as [tenacity](https://github.com/jd/tenacity).
```pycon
>>> import httpx2
>>> transport = httpx2.HTTPTransport(retries=1)
>>> client = httpx2.Client(transport=transport)
```
Similarly, instantiating a transport directly provides a `uds` option for
connecting via a Unix Domain Socket that is only available via this low-level API:
```pycon
>>> import httpx2
>>> # Connect to the Docker API via a Unix Socket.
>>> transport = httpx2.HTTPTransport(uds="/var/run/docker.sock")
>>> client = httpx2.Client(transport=transport)
>>> response = client.get("http://docker/info")
>>> response.json()
{"ID": "...", "Containers": 4, "Images": 74, ...}
```
## WSGI Transport
You can configure an `httpx2` client to call directly into a Python web application using the WSGI protocol.
This is particularly useful for two main use-cases:
* Using `httpx2` as a client inside test cases.
* Mocking out external services during tests or in dev or staging environments.
### Example
Here's an example of integrating against a Flask application:
```python
from flask import Flask
import httpx2
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello World!"
transport = httpx2.WSGITransport(app=app)
with httpx2.Client(transport=transport, base_url="http://testserver") as client:
r = client.get("/")
assert r.status_code == 200
assert r.text == "Hello World!"
```
### Configuration
For some more complex cases you might need to customize the WSGI transport. This allows you to:
* Inspect 500 error responses rather than raise exceptions by setting `raise_app_exceptions=False`.
* Mount the WSGI application at a subpath by setting `script_name` (WSGI).
* Use a given client address for requests by setting `remote_addr` (WSGI).
For example:
```python
# Instantiate a client that makes WSGI requests with a client IP of "1.2.3.4".
transport = httpx2.WSGITransport(app=app, remote_addr="1.2.3.4")
with httpx2.Client(transport=transport, base_url="http://testserver") as client:
...
```
## ASGI Transport
You can configure an `httpx2` client to call directly into an async Python web application using the ASGI protocol.
This is particularly useful for two main use-cases:
* Using `httpx2` as a client inside test cases.
* Mocking out external services during tests or in dev or staging environments.
### Example
Let's take this Starlette application as an example:
```python
from starlette.applications import Starlette
from starlette.responses import HTMLResponse
from starlette.routing import Route
async def hello(request):
return HTMLResponse("Hello World!")
app = Starlette(routes=[Route("/", hello)])
```
We can make requests directly against the application, like so:
```python
transport = httpx2.ASGITransport(app=app)
async with httpx2.AsyncClient(transport=transport, base_url="http://testserver") as client:
r = await client.get("/")
assert r.status_code == 200
assert r.text == "Hello World!"
```
### Configuration
For some more complex cases you might need to customise the ASGI transport. This allows you to:
* Inspect 500 error responses rather than raise exceptions by setting `raise_app_exceptions=False`.
* Mount the 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 ASGI requests with a client IP of "1.2.3.4",
# on port 123.
transport = httpx2.ASGITransport(app=app, client=("1.2.3.4", 123))
async with httpx2.AsyncClient(transport=transport, base_url="http://testserver") as client:
...
```
See [the ASGI documentation](https://asgi.readthedocs.io/en/latest/specs/www.html#connection-scope) for more details on the `client` and `root_path` keys.
### ASGI startup and shutdown
It is not in the scope of HTTPX to trigger ASGI lifespan events of your app.
However it is suggested to use `LifespanManager` from [asgi-lifespan](https://github.com/florimondmanca/asgi-lifespan#usage) in pair with `AsyncClient`.
## Custom transports
A transport instance must implement the low-level Transport API which deals
with sending a single request, and returning a response. You should either
subclass `httpx2.BaseTransport` to implement a transport to use with `Client`,
or subclass `httpx2.AsyncBaseTransport` to implement a transport to
use with `AsyncClient`.
At the layer of the transport API we're using the familiar `Request` and
`Response` models.
See the `handle_request` and `handle_async_request` docstrings for more details
on the specifics of the Transport API.
A complete example of a custom transport implementation would be:
```python
import json
import httpx2
class HelloWorldTransport(httpx2.BaseTransport):
"""
A mock transport that always returns a JSON "Hello, world!" response.
"""
def handle_request(self, request):
return httpx2.Response(200, json={"text": "Hello, world!"})
```
Or this example, which uses a custom transport and the `mounts` argument to always redirect `http://` requests.
```python
class HTTPSRedirect(httpx2.BaseTransport):
"""
A transport that always redirects to HTTPS.
"""
def handle_request(self, request):
url = request.url.copy_with(scheme="https")
return httpx2.Response(303, headers={"Location": str(url)})
# A client where any `http` requests are always redirected to `https`
client = httpx2.Client(
mounts={
"http://": HTTPSRedirect(),
"https://": httpx2.HTTPTransport(),
},
follow_redirects=True,
)
```
A useful pattern here is custom transport classes that wrap the default HTTP implementation. For example...
```python
class DebuggingTransport(httpx2.BaseTransport):
def __init__(self, **kwargs):
self._wrapper = httpx2.HTTPTransport(**kwargs)
def handle_request(self, request):
print(f">>> {request}")
response = self._wrapper.handle_request(request)
print(f"<<< {response}")
return response
def close(self):
self._wrapper.close()
transport = DebuggingTransport()
client = httpx2.Client(transport=transport)
```
Here's another case, where we're using a round-robin across a number of different proxies...
```python
class ProxyRoundRobin(httpx2.BaseTransport):
def __init__(self, proxies, **kwargs):
self._transports = [
httpx2.HTTPTransport(proxy=proxy, **kwargs)
for proxy in proxies
]
self._idx = 0
def handle_request(self, request):
transport = self._transports[self._idx]
self._idx = (self._idx + 1) % len(self._transports)
return transport.handle_request(request)
def close(self):
for transport in self._transports:
transport.close()
proxies = [
httpx2.Proxy("http://127.0.0.1:8081"),
httpx2.Proxy("http://127.0.0.1:8082"),
httpx2.Proxy("http://127.0.0.1:8083"),
]
transport = ProxyRoundRobin(proxies=proxies)
client = httpx2.Client(transport=transport)
```
## Mock transports
During testing it can often be useful to be able to mock out a transport,
and return pre-determined responses, rather than making actual network requests.
The `httpx2.MockTransport` class accepts a handler function, which can be used
to map requests onto pre-determined responses:
```python
def handler(request):
return httpx2.Response(200, json={"text": "Hello, world!"})
# Switch to a mock transport, if the TESTING environment variable is set.
if os.environ.get('TESTING', '').upper() == "TRUE":
transport = httpx2.MockTransport(handler)
else:
transport = httpx2.HTTPTransport()
client = httpx2.Client(transport=transport)
```
For more advanced mocking, see [httpx2-pytest](../third_party_packages.md#httpx2-pytest)
or [pytest-HTTPX2](../third_party_packages.md#pytest-httpx2).
## Mounting transports
You can also mount transports against given schemes or domains, to control
which transport an outgoing request should be routed via, with [the same style
used for specifying proxy routing](#routing).
```python
import httpx2
class HTTPSRedirectTransport(httpx2.BaseTransport):
"""
A transport that always redirects to HTTPS.
"""
def handle_request(self, method, url, headers, stream, extensions):
scheme, host, port, path = url
if port is None:
location = b"https://%s%s" % (host, path)
else:
location = b"https://%s:%d%s" % (host, port, path)
stream = httpx2.ByteStream(b"")
headers = [(b"location", location)]
extensions = {}
return 303, headers, stream, extensions
# A client where any `http` requests are always redirected to `https`
mounts = {'http://': HTTPSRedirectTransport()}
client = httpx2.Client(mounts=mounts)
```
A couple of other sketches of how you might take advantage of mounted transports...
Disabling HTTP/2 on a single given domain...
```python
mounts = {
"all://": httpx2.HTTPTransport(http2=True),
"all://*example.org": httpx2.HTTPTransport()
}
client = httpx2.Client(mounts=mounts)
```
Mocking requests to a given domain:
```python
# All requests to "example.org" should be mocked out.
# Other requests occur as usual.
def handler(request):
return httpx2.Response(200, json={"text": "Hello, World!"})
mounts = {"all://example.org": httpx2.MockTransport(handler)}
client = httpx2.Client(mounts=mounts)
```
Adding support for custom schemes:
```python
# Support URLs like "file:///Users/sylvia_green/websites/new_client/index.html"
mounts = {"file://": FileSystemTransport()}
client = httpx2.Client(mounts=mounts)
```
### Routing
HTTPX provides a powerful mechanism for routing requests, allowing you to write complex rules that specify which transport should be used for each request.
The `mounts` dictionary maps URL patterns to HTTP transports. HTTPX matches requested URLs against URL patterns to decide which transport should be used, if any. Matching is done from most specific URL patterns (e.g. `https://:`) to least specific ones (e.g. `https://`).
HTTPX supports routing requests based on **scheme**, **domain**, **port**, or a combination of these.
### Wildcard routing
Route everything through a transport...
```python
mounts = {
"all://": httpx2.HTTPTransport(proxy="http://localhost:8030"),
}
```
### Scheme routing
Route HTTP requests through one transport, and HTTPS requests through another...
```python
mounts = {
"http://": httpx2.HTTPTransport(proxy="http://localhost:8030"),
"https://": httpx2.HTTPTransport(proxy="http://localhost:8031"),
}
```
### Domain routing
Proxy all requests on domain "example.com", let other requests pass through...
```python
mounts = {
"all://example.com": httpx2.HTTPTransport(proxy="http://localhost:8030"),
}
```
Proxy HTTP requests on domain "example.com", let HTTPS and other requests pass through...
```python
mounts = {
"http://example.com": httpx2.HTTPTransport(proxy="http://localhost:8030"),
}
```
Proxy all requests to "example.com" and its subdomains, let other requests pass through...
```python
mounts = {
"all://*example.com": httpx2.HTTPTransport(proxy="http://localhost:8030"),
}
```
Proxy all requests to strict subdomains of "example.com", let "example.com" and other requests pass through...
```python
mounts = {
"all://*.example.com": httpx2.HTTPTransport(proxy="http://localhost:8030"),
}
```
### Port routing
Proxy HTTPS requests on port 1234 to "example.com"...
```python
mounts = {
"https://example.com:1234": httpx2.HTTPTransport(proxy="http://localhost:8030"),
}
```
Proxy all requests on port 1234...
```python
mounts = {
"all://*:1234": httpx2.HTTPTransport(proxy="http://localhost:8030"),
}
```
### No-proxy support
It is also possible to define requests that _shouldn't_ be routed through the transport.
To do so, pass `None` as the proxy URL. For example...
```python
mounts = {
# Route requests through a proxy by default...
"all://": httpx2.HTTPTransport(proxy="http://localhost:8031"),
# Except those for "example.com".
"all://example.com": None,
}
```
### Complex configuration example
You can combine the routing features outlined above to build complex proxy routing configurations. For example...
```python
mounts = {
# Route all traffic through a proxy by default...
"all://": httpx2.HTTPTransport(proxy="http://localhost:8030"),
# But don't use proxies for HTTPS requests to "domain.io"...
"https://domain.io": None,
# And use another proxy for requests to "example.com" and its subdomains...
"all://*example.com": httpx2.HTTPTransport(proxy="http://localhost:8031"),
# And yet another proxy if HTTP is used,
# and the "internal" subdomain on port 5550 is requested...
"http://internal.example.com:5550": httpx2.HTTPTransport(proxy="http://localhost:8032"),
}
```
### Environment variables
There are also environment variables that can be used to control the dictionary of the client mounts.
They can be used to configure HTTP proxying for clients.
See documentation on [`HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`](../environment_variables.md#http_proxy-https_proxy-all_proxy)
and [`NO_PROXY`](../environment_variables.md#no_proxy) for more information.
pydantic-httpx2-6dd59b9/docs/api.md 0000664 0000000 0000000 00000013715 15265474634 0017256 0 ustar 00root root 0000000 0000000 # Developer Interface
## Helper Functions
!!! note
Only use these functions if you're testing HTTPX in a console
or making a small number of requests. Using a `Client` will
enable HTTP/2 and connection pooling for more efficient and
long-lived connections.
::: httpx2.request
::: httpx2.get
::: httpx2.options
::: httpx2.head
::: httpx2.post
::: httpx2.put
::: httpx2.patch
::: httpx2.delete
::: httpx2.query
::: httpx2.stream
::: httpx2.websocket
## `Client`
::: httpx2.Client
options:
members:
- headers
- cookies
- params
- auth
- request
- get
- head
- options
- post
- put
- patch
- delete
- query
- stream
- sse
- websocket
- build_request
- send
- close
## `AsyncClient`
::: httpx2.AsyncClient
options:
members:
- headers
- cookies
- params
- auth
- request
- get
- head
- options
- post
- put
- patch
- delete
- query
- stream
- sse
- websocket
- build_request
- send
- aclose
## `Response`
*An HTTP response.*
* `def __init__(...)`
* `.status_code` - **int**
* `.reason_phrase` - **str**
* `.http_version` - `"HTTP/2"` or `"HTTP/1.1"`
* `.url` - **URL**
* `.headers` - **Headers**
* `.content` - **bytes**
* `.text` - **str**
* `.encoding` - **str**
* `.is_redirect` - **bool**
* `.request` - **Request**
* `.next_request` - **Optional[Request]**
* `.cookies` - **Cookies**
* `.history` - **List[Response]**
* `.elapsed` - **[timedelta](https://docs.python.org/3/library/datetime.html)**
* The amount of time elapsed between sending the request and calling `close()` on the corresponding response received for that request.
[total_seconds()](https://docs.python.org/3/library/datetime.html#datetime.timedelta.total_seconds) to correctly get
the total elapsed seconds.
* `def .raise_for_status()` - **Response**
* `def .json()` - **Any**
* `def .read()` - **bytes**
* `def .iter_raw([chunk_size])` - **bytes iterator**
* `def .iter_bytes([chunk_size])` - **bytes iterator**
* `def .iter_text([chunk_size])` - **text iterator**
* `def .iter_lines()` - **text iterator**
* `def .close()` - **None**
* `def .next()` - **Response**
* `def .aread()` - **bytes**
* `def .aiter_raw([chunk_size])` - **async bytes iterator**
* `def .aiter_bytes([chunk_size])` - **async bytes iterator**
* `def .aiter_text([chunk_size])` - **async text iterator**
* `def .aiter_lines()` - **async text iterator**
* `def .aclose()` - **None**
* `def .anext()` - **Response**
## `Request`
*An HTTP request. Can be constructed explicitly for more control over exactly
what gets sent over the wire.*
```pycon
>>> request = httpx2.Request("GET", "https://example.org", headers={'host': 'example.org'})
>>> response = client.send(request)
```
* `def __init__(method, url, [params], [headers], [cookies], [content], [data], [files], [json], [stream])`
* `.method` - **str**
* `.url` - **URL**
* `.content` - **byte**, **byte iterator**, or **byte async iterator**
* `.headers` - **Headers**
* `.cookies` - **Cookies**
## `URL`
*A normalized, IDNA supporting URL.*
```pycon
>>> url = URL("https://example.org/")
>>> url.host
'example.org'
```
* `def __init__(url, **kwargs)`
* `.scheme` - **str**
* `.authority` - **str**
* `.host` - **str**
* `.port` - **int**
* `.path` - **str**
* `.query` - **str**
* `.raw_path` - **str**
* `.fragment` - **str**
* `.origin` - **Origin**
* `.is_ssl` - **bool**
* `.is_absolute_url` - **bool**
* `.is_relative_url` - **bool**
* `def .copy_with([scheme], [authority], [path], [query], [fragment])` - **URL**
## `Origin`
*An immutable, hashable set of normalized scheme, host, and effective port information.*
```pycon
>>> URL('https://example.org').origin == URL('HTTPS://EXAMPLE.ORG:443').origin
True
>>> URL('wss://[::1]/socket').origin
Origin(scheme='wss', host='::1', port=443)
```
* `def __init__(url)`
* `.scheme` - **str**
* `.host` - **str**
* `.port` - **int** or **None**
## `Headers`
*A case-insensitive multi-dict.*
```pycon
>>> headers = Headers({'Content-Type': 'application/json'})
>>> headers['content-type']
'application/json'
```
* `def __init__(self, headers, encoding=None)`
* `def copy()` - **Headers**
## `Cookies`
*A dict-like cookie store.*
```pycon
>>> cookies = Cookies()
>>> cookies.set("name", "value", domain="example.org")
```
* `def __init__(cookies: [dict, Cookies, CookieJar])`
* `.jar` - **CookieJar**
* `def extract_cookies(response)`
* `def set_cookie_header(request)`
* `def set(name, value, [domain], [path])`
* `def get(name, [domain], [path])`
* `def delete(name, [domain], [path])`
* `def clear([domain], [path])`
* *Standard mutable mapping interface*
## `Proxy`
*A configuration of the proxy server.*
```pycon
>>> proxy = Proxy("http://proxy.example.com:8030")
>>> client = Client(proxy=proxy)
```
* `def __init__(url, [ssl_context], [auth], [headers])`
* `.url` - **URL**
* `.auth` - **tuple[str, str]**
* `.headers` - **Headers**
* `.ssl_context` - **SSLContext**
## `EventSource`
::: httpx2.EventSource
options:
members:
- response
## `ServerSentEvent`
::: httpx2.ServerSentEvent
options:
members:
- event
- data
- id
- retry
- json
## `WebSocketSession`
::: httpx2.websockets.WebSocketSession
options:
members:
- subprotocol
- response
- send_text
- send_bytes
- send_json
- receive_text
- receive_bytes
- receive_json
- ping
- close
## `AsyncWebSocketSession`
::: httpx2.websockets.AsyncWebSocketSession
options:
members:
- subprotocol
- response
- send_text
- send_bytes
- send_json
- receive_text
- receive_bytes
- receive_json
- ping
- close
## `alias_httpx`
::: httpx2.alias_httpx
pydantic-httpx2-6dd59b9/docs/async.md 0000664 0000000 0000000 00000014475 15265474634 0017626 0 ustar 00root root 0000000 0000000 # Async Support
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`.
```pycon
>>> async with httpx2.AsyncClient() as client:
... r = await client.get('https://www.example.com/')
...
>>> r
```
!!! tip
Use [IPython](https://ipython.readthedocs.io/en/stable/) or Python with `python -m asyncio` to try this code interactively, as they support executing `async`/`await` expressions in the console.
## API Differences
If you're using an async client then there are a few bits of API that
use async methods.
### Making requests
The request methods are all async, so you should use `response = await client.get(...)` style for all of the following:
* `AsyncClient.get(url, ...)`
* `AsyncClient.options(url, ...)`
* `AsyncClient.head(url, ...)`
* `AsyncClient.post(url, ...)`
* `AsyncClient.put(url, ...)`
* `AsyncClient.patch(url, ...)`
* `AsyncClient.delete(url, ...)`
* `AsyncClient.query(url, ...)`
* `AsyncClient.request(method, url, ...)`
* `AsyncClient.send(request, ...)`
### Opening and closing clients
Use `async with httpx2.AsyncClient()` if you want a context-managed client...
```python
async with httpx2.AsyncClient() as client:
...
```
!!! warning
In order to get the most benefit from connection pooling, make sure you're not instantiating multiple client instances - for example by using `async with` inside a "hot loop". This can be achieved either by having a single scoped client that's passed throughout wherever it's needed, or by having a single global client instance.
Alternatively, use `await client.aclose()` if you want to close a client explicitly:
```python
client = httpx2.AsyncClient()
...
await client.aclose()
```
### Streaming responses
The `AsyncClient.stream(method, url, ...)` method is an async context block.
```pycon
>>> client = httpx2.AsyncClient()
>>> async with client.stream('GET', 'https://www.example.com/') as response:
... async for chunk in response.aiter_bytes():
... ...
```
The async response streaming methods are:
* `Response.aread()` - For conditionally reading a response inside a stream block.
* `Response.aiter_bytes()` - For streaming the response content as bytes.
* `Response.aiter_text()` - For streaming the response content as text.
* `Response.aiter_lines()` - For streaming the response content as lines of text.
* `Response.aiter_raw()` - For streaming the raw response bytes, without applying content decoding.
* `Response.aclose()` - For closing the response. You don't usually need this, since `.stream` block closes the response automatically on exit.
For situations when context block usage is not practical, it is possible to enter "manual mode" by sending a [`Request` instance](advanced/clients.md#request-instances) using `client.send(..., stream=True)`.
Example in the context of forwarding the response to a streaming web endpoint with [Starlette](https://www.starlette.io):
```python
import httpx2
from starlette.background import BackgroundTask
from starlette.responses import StreamingResponse
client = httpx2.AsyncClient()
async def home(request):
req = client.build_request("GET", "https://www.example.com/")
r = await client.send(req, stream=True)
return StreamingResponse(r.aiter_text(), background=BackgroundTask(r.aclose))
```
!!! warning
When using this "manual streaming mode", it is your duty as a developer to make sure that `Response.aclose()` is called eventually. Failing to do so would leave connections open, most likely resulting in resource leaks down the line.
### Streaming requests
When sending a streaming request body with an `AsyncClient` instance, you should use an async bytes generator instead of a bytes generator:
```python
async def upload_bytes():
... # yield byte content
await client.post(url, content=upload_bytes())
```
### Explicit transport instances
When instantiating a transport instance directly, you need to use `httpx2.AsyncHTTPTransport`.
For instance:
```pycon
>>> import httpx2
>>> transport = httpx2.AsyncHTTPTransport(retries=1)
>>> async with httpx2.AsyncClient(transport=transport) as client:
>>> ...
```
## Supported async environments
HTTPX supports either `asyncio` or `trio` as an async environment.
It will auto-detect which of those two to use as the backend
for socket operations and concurrency primitives.
### [AsyncIO](https://docs.python.org/3/library/asyncio.html)
AsyncIO is Python's [built-in library](https://docs.python.org/3/library/asyncio.html)
for writing concurrent code with the async/await syntax.
```python
import asyncio
import httpx2
async def main():
async with httpx2.AsyncClient() as client:
response = await client.get('https://www.example.com/')
print(response)
asyncio.run(main())
```
### [Trio](https://github.com/python-trio/trio)
Trio is [an alternative async library](https://trio.readthedocs.io/en/stable/),
designed around the [the principles of structured concurrency](https://en.wikipedia.org/wiki/Structured_concurrency).
```python
import httpx2
import trio
async def main():
async with httpx2.AsyncClient() as client:
response = await client.get('https://www.example.com/')
print(response)
trio.run(main)
```
!!! important
The `trio` package must be installed to use the Trio backend.
### [AnyIO](https://github.com/agronholm/anyio)
AnyIO is an [asynchronous networking and concurrency library](https://anyio.readthedocs.io/) that works on top of either `asyncio` or `trio`. It blends in with native libraries of your chosen backend (defaults to `asyncio`).
```python
import httpx2
import anyio
async def main():
async with httpx2.AsyncClient() as client:
response = await client.get('https://www.example.com/')
print(response)
anyio.run(main, backend='trio')
```
## Calling into Python Web Apps
For details on calling directly into ASGI applications, see [the `ASGITransport` docs](../advanced/transports#asgitransport).
pydantic-httpx2-6dd59b9/docs/code_of_conduct.md 0000664 0000000 0000000 00000011032 15265474634 0021610 0 ustar 00root root 0000000 0000000 # Code of Conduct
We expect contributors to our projects and online spaces to follow [the Python Software Foundationโs Code of Conduct](https://www.python.org/psf/conduct/).
The Python community is made up of members from around the globe with a diverse set of skills, personalities, and experiences. It is through these differences that our community experiences great successes and continued growth. When you're working with members of the community, this Code of Conduct will help steer your interactions and keep Python a positive, successful, and growing community.
## Our Community
Members of the Python community are **open, considerate, and respectful**. Behaviours that reinforce these values contribute to a positive environment, and include:
* **Being open.** Members of the community are open to collaboration, whether it's on PEPs, patches, problems, or otherwise.
* **Focusing on what is best for the community.** We're respectful of the processes set forth in the community, and we work within them.
* **Acknowledging time and effort.** We're respectful of the volunteer efforts that permeate the Python community. We're thoughtful when addressing the efforts of others, keeping in mind that often times the labor was completed simply for the good of the community.
* **Being respectful of differing viewpoints and experiences.** We're receptive to constructive comments and criticism, as the experiences and skill sets of other members contribute to the whole of our efforts.
* **Showing empathy towards other community members.** We're attentive in our communications, whether in person or online, and we're tactful when approaching differing views.
* **Being considerate.** Members of the community are considerate of their peers -- other Python users.
* **Being respectful.** We're respectful of others, their positions, their skills, their commitments, and their efforts.
* **Gracefully accepting constructive criticism.** When we disagree, we are courteous in raising our issues.
* **Using welcoming and inclusive language.** We're accepting of all who wish to take part in our activities, fostering an environment where anyone can participate and everyone can make a difference.
## Our Standards
Every member of our community has the right to have their identity respected. The Python community is dedicated to providing a positive experience for everyone, regardless of age, gender identity and expression, sexual orientation, disability, physical appearance, body size, ethnicity, nationality, race, or religion (or lack thereof), education, or socio-economic status.
## Inappropriate Behavior
Examples of unacceptable behavior by participants include:
* Harassment of any participants in any form
* Deliberate intimidation, stalking, or following
* Logging or taking screenshots of online activity for harassment purposes
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Violent threats or language directed against another person
* Incitement of violence or harassment towards any individual, including encouraging a person to commit suicide or to engage in self-harm
* Creating additional online accounts in order to harass another person or circumvent a ban
* Sexual language and imagery in online communities or in any conference venue, including talks
* Insults, put downs, or jokes that are based upon stereotypes, that are exclusionary, or that hold others up for ridicule
* Excessive swearing
* Unwelcome sexual attention or advances
* Unwelcome physical contact, including simulated physical contact (eg, textual descriptions like "hug" or "backrub") without consent or after a request to stop
* Pattern of inappropriate social contact, such as requesting/assuming inappropriate levels of intimacy with others
* Sustained disruption of online community discussions, in-person presentations, or other in-person events
* Continued one-on-one communication after requests to cease
* Other conduct that is inappropriate for a professional audience including people of many different backgrounds
Community members asked to stop any inappropriate behavior are expected to comply immediately.
## Enforcement
We take Code of Conduct violations seriously, and will act to ensure our spaces are welcoming, inclusive, and professional environments to communicate in.
If you need to raise a Code of Conduct report, you may do so privately by email to hello@pydantic.dev.
Reports will be treated confidentially.
Alternately you may [make a report to the Python Software Foundation](https://www.python.org/psf/conduct/reporting/).
pydantic-httpx2-6dd59b9/docs/compatibility.md 0000664 0000000 0000000 00000023121 15265474634 0021346 0 ustar 00root root 0000000 0000000 # Requests Compatibility Guide
HTTPX aims to be broadly compatible with the `requests` API, although there are a
few design differences in places.
This documentation outlines places where the API differs...
## Redirects
Unlike `requests`, HTTPX does **not follow redirects by default**.
We differ in behaviour here [because auto-redirects can easily mask unnecessary network
calls being made](https://github.com/encode/httpx/discussions/1785).
You can still enable behaviour to automatically follow redirects, but you need to
do so explicitly...
```python
response = client.get(url, follow_redirects=True)
```
Or else instantiate a client, with redirect following enabled by default...
```python
client = httpx2.Client(follow_redirects=True)
```
## Client instances
The HTTPX equivalent of `requests.Session` is `httpx2.Client`.
```python
session = requests.Session(**kwargs)
```
is generally equivalent to
```python
client = httpx2.Client(**kwargs)
```
## Request URLs
Accessing `response.url` will return a `URL` instance, rather than a string.
Use `str(response.url)` if you need a string instance.
## Determining the next redirect request
The `requests` library exposes an attribute `response.next`, which can be used to obtain the next redirect request.
```python
session = requests.Session()
request = requests.Request("GET", ...).prepare()
while request is not None:
response = session.send(request, allow_redirects=False)
request = response.next
```
In HTTPX, this attribute is instead named `response.next_request`. For example:
```python
client = httpx2.Client()
request = client.build_request("GET", ...)
while request is not None:
response = client.send(request)
request = response.next_request
```
## Request Content
For uploading raw text or binary content we prefer to use a `content` parameter,
in order to better separate this usage from the case of uploading form data.
For example, using `content=...` to upload raw content:
```python
# Uploading text, bytes, or a bytes iterator.
httpx2.post(..., content=b"Hello, world")
```
And using `data=...` to send form data:
```python
# Uploading form data.
httpx2.post(..., data={"message": "Hello, world"})
```
Using the `data=` will raise a deprecation warning,
and is expected to be fully removed with the HTTPX 1.0 release.
## Upload files
HTTPX strictly enforces that upload files must be opened in binary mode, in order
to avoid character encoding issues that can result from attempting to upload files
opened in text mode.
## Content encoding
HTTPX uses `utf-8` for encoding `str` request bodies. For example, when using `content=` the request body will be encoded to `utf-8` before being sent over the wire. This differs from Requests which uses `latin1`. If you need an explicit encoding, pass encoded bytes explicitly, e.g. `content=.encode("latin1")`.
For response bodies, assuming the server didn't send an explicit encoding then HTTPX will do its best to figure out an appropriate encoding. HTTPX makes a guess at the encoding to use for decoding the response using `charset_normalizer`. Fallback to that or any content with less than 32 octets will be decoded using `utf-8` with the `error="replace"` decoder strategy.
## Cookies
If using a client instance, then cookies should always be set on the client rather than on a per-request basis.
This usage is supported:
```python
client = httpx2.Client(cookies=...)
client.post(...)
```
This usage is **not** supported:
```python
client = httpx2.Client()
client.post(..., cookies=...)
```
We prefer enforcing a stricter API here because it provides clearer expectations around cookie persistence, particularly when redirects occur.
## Status Codes
In our documentation we prefer the uppercased versions, such as `codes.NOT_FOUND`, but also provide lower-cased versions for API compatibility with `requests`.
Requests includes various synonyms for status codes that HTTPX does not support.
## Streaming responses
HTTPX provides a `.stream()` interface rather than using `stream=True`. This ensures that streaming responses are always properly closed outside of the stream block, and makes it visually clearer at which points streaming I/O APIs may be used with a response.
For example:
```python
with httpx2.stream("GET", "https://www.example.com") as response:
...
```
Within a `stream()` block request data is made available with:
* `.iter_bytes()` - Instead of `response.iter_content()`
* `.iter_text()` - Instead of `response.iter_content(decode_unicode=True)`
* `.iter_lines()` - Corresponding to `response.iter_lines()`
* `.iter_raw()` - Use this instead of `response.raw`
* `.read()` - Read the entire response body, making `response.text` and `response.content` available.
## Timeouts
HTTPX defaults to including reasonable [timeouts](quickstart.md#timeouts) for all network operations, while Requests has no timeouts by default.
To get the same behavior as Requests, set the `timeout` parameter to `None`:
```python
httpx2.get('https://www.example.com', timeout=None)
```
## Proxy keys
HTTPX uses the mounts argument for HTTP proxying and transport routing.
It can do much more than proxies and allows you to configure more than just the proxy route.
For more detailed documentation, see [Mounting Transports](advanced/transports.md#mounting-transports).
When using `httpx2.Client(mounts={...})` to map to a selection of different transports, we use full URL schemes, such as `mounts={"http://": ..., "https://": ...}`.
This is different to the `requests` usage of `proxies={"http": ..., "https": ...}`.
This change is for better consistency with more complex mappings, that might also include domain names, such as `mounts={"all://": ..., httpx2.HTTPTransport(proxy="all://www.example.com": None})` which maps all requests onto a proxy, except for requests to "www.example.com" which have an explicit exclusion.
Also note that `requests.Session.request(...)` allows a `proxies=...` parameter, whereas `httpx2.Client.request(...)` does not allow `mounts=...`.
## SSL configuration
When using a `Client` instance, the ssl configurations should always be passed on client instantiation, rather than passed to the request method.
If you need more than one different SSL configuration, you should use different client instances for each SSL configuration.
## Request body on HTTP methods
The HTTP `GET`, `DELETE`, `HEAD`, and `OPTIONS` methods are specified as not supporting a request body. To stay in line with this, the `.get`, `.delete`, `.head` and `.options` functions do not support `content`, `files`, `data`, or `json` arguments.
If you really do need to send request data using these http methods you should use the generic `.request` function instead.
```python
httpx2.request(
method="DELETE",
url="https://www.example.com/",
content=b'A request body on a DELETE request.'
)
```
## Checking for success and failure responses
We don't support `response.is_ok` since the naming is ambiguous there, and might incorrectly imply an equivalence to `response.status_code == codes.OK`. Instead we provide the `response.is_success` property, which can be used to check for a 2xx response.
## Request instantiation
There is no notion of [prepared requests](https://requests.readthedocs.io/en/stable/user/advanced/#prepared-requests) in HTTPX. If you need to customize request instantiation, see [Request instances](advanced/clients.md#request-instances).
Besides, `httpx2.Request()` does not support the `auth`, `timeout`, `follow_redirects`, `mounts`, `verify` and `cert` parameters. However these are available in `httpx2.request`, `httpx2.get`, `httpx2.post` etc., as well as on [`Client` instances](advanced/clients.md#sharing-configuration-across-requests).
## Mocking
If you need to mock HTTPX2 requests in your tests, see [httpx2-pytest](third_party_packages.md#httpx2-pytest)
or [pytest-HTTPX2](third_party_packages.md#pytest-httpx2).
## Networking layer
`requests` defers most of its HTTP networking code to the excellent [`urllib3` library](https://urllib3.readthedocs.io/en/latest/).
On the other hand, HTTPX2 uses [`httpcore2`](https://github.com/pydantic/httpx2/tree/main/src/httpcore2) as its core HTTP networking layer, which is a different project than `urllib3`.
## Query Parameters
`requests` omits `params` whose values are `None` (e.g. `requests.get(..., params={"foo": None})`). This is not supported by HTTPX.
For both query params (`params=`) and form data (`data=`), `requests` supports sending a list of tuples (e.g. `requests.get(..., params=[('key1', 'value1'), ('key1', 'value2')])`). This is not supported by HTTPX. Instead, use a dictionary with lists as values. E.g.: `httpx2.get(..., params={'key1': ['value1', 'value2']})` or with form data: `httpx2.post(..., data={'key1': ['value1', 'value2']})`.
## Event Hooks
`requests` allows event hooks to mutate `Request` and `Response` objects. See [examples](https://requests.readthedocs.io/en/master/user/advanced/#event-hooks) given in the documentation for `requests`.
In HTTPX, event hooks may access properties of requests and responses, but event hook callbacks cannot mutate the original request/response.
If you are looking for more control, consider checking out [Custom Transports](advanced/transports.md#custom-transports).
## Exceptions and Errors
`requests` exception hierarchy is slightly different to the `httpx2` exception hierarchy. `requests` exposes a top level `RequestException`, where as `httpx2` exposes a top level `HTTPError`. see the exceptions exposes in requests [here](https://requests.readthedocs.io/en/latest/_modules/requests/exceptions/). See the `httpx2` error hierarchy [here](https://pydantic.dev/docs/httpx2/api/exceptions/).
pydantic-httpx2-6dd59b9/docs/contributing.md 0000664 0000000 0000000 00000020200 15265474634 0021177 0 ustar 00root root 0000000 0000000 # Contributing
Thank you for being interested in contributing to HTTPX.
There are many ways you can contribute to the project:
- Try HTTPX and [report bugs/issues you find](https://github.com/pydantic/httpx2/issues/new)
- [Implement new features](https://github.com/pydantic/httpx2/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
- [Review Pull Requests of others](https://github.com/pydantic/httpx2/pulls)
- Write documentation
- Participate in discussions
## Reporting Bugs or Other Issues
Found something that HTTPX2 should support?
Stumbled upon some unexpected behaviour?
Contributions should generally start out with [a discussion](https://github.com/pydantic/httpx2/discussions).
Possible bugs and feature requests may be raised in the "Q&A" category. We can then
determine if the discussion needs to be escalated into an "Issue" or not, or if we'd
consider a pull request.
Try to be more descriptive as you can and in case of a bug report,
provide as much information as possible like:
- OS platform
- Python version
- Installed dependencies and versions (`python -m pip freeze`)
- Code snippet
- Error traceback
You should always try to reduce any examples to the *simplest possible case*
that demonstrates the issue.
Some possibly useful tips for narrowing down potential issues...
- Does the issue exist on HTTP/1.1, or HTTP/2, or both?
- Does the issue exist with `Client`, `AsyncClient`, or both?
- When using `AsyncClient` does the issue exist when using `asyncio` or `trio`, or both?
## Development
To start developing HTTPX create a **fork** of the
[HTTPX repository](https://github.com/pydantic/httpx2) on GitHub.
Then clone your fork with the following command replacing `YOUR-USERNAME` with
your GitHub username:
```shell
$ git clone https://github.com/YOUR-USERNAME/httpx2
```
You can now install the project and its dependencies using:
```shell
$ cd httpx2
$ scripts/install
```
## Testing and Linting
We use custom shell scripts to automate testing, linting,
and documentation building workflow.
To run the tests, use:
```shell
$ scripts/test
```
Any additional arguments will be passed to `pytest`. See the [pytest documentation](https://docs.pytest.org/en/latest/how-to/usage.html) for more information.
For example, to run a single test script:
```shell
$ scripts/test tests/test_multipart.py
```
To run the code auto-formatting:
```shell
$ scripts/lint
```
Lastly, to run code checks separately (they are also run as part of `scripts/test`), run:
```shell
$ scripts/check
```
## Documenting
Documentation pages are located under the `docs/` folder.
To run the documentation site locally (useful for previewing changes), use:
```shell
$ scripts/docs
```
## Resolving Build / CI Failures
Once you've submitted your pull request, the test suite will automatically run, and the results will show up in GitHub.
If the test suite fails, you'll want to click through to the "Details" link, and try to identify why the test suite failed.
Here are some common ways the test suite can fail:
### Check Job Failed
This job failing means there is either a code formatting issue or type-annotation issue.
You can look at the job output to figure out why it's failed or within a shell run:
```shell
$ scripts/check
```
It may be worth it to run `$ scripts/lint` to attempt auto-formatting the code
and if that job succeeds commit the changes.
### Docs Job Failed
This job failing means the documentation failed to build. This can happen for
a variety of reasons like invalid markdown or missing configuration within `mkdocs.yml`.
### Python 3.X Job Failed
This job failing means the unit tests failed or not all code paths are covered by unit tests.
If tests are failing you will see this message under the coverage report:
`=== 1 failed, 435 passed, 1 skipped, 1 xfailed in 11.09s ===`
If tests succeed but coverage doesn't reach our current threshold, you will see this
message under the coverage report:
`FAIL Required test coverage of 100% not reached. Total coverage: 99.00%`
## Releasing
*This section is targeted at HTTPX maintainers.*
Before releasing a new version, create a pull request that includes:
- **An update to the changelog**:
- We follow the format from [keepachangelog](https://keepachangelog.com/en/1.0.0/).
- [Compare](https://github.com/pydantic/httpx2/compare/) `master` with the tag of the latest release, and list all entries that are of interest to our users:
- Things that **must** go in the changelog: added, changed, deprecated or removed features, and bug fixes.
- Things that **should not** go in the changelog: changes to documentation, tests or tooling.
- Try sorting entries in descending order of impact / importance.
- Keep it concise and to-the-point. ๐ฏ
- **A version bump**: see `__version__.py`.
For an example, see [#1006](https://github.com/encode/httpx/pull/1006).
Once the release PR is merged, create a
[new release](https://github.com/pydantic/httpx2/releases/new) including:
- Tag version like `0.13.3`.
- Release title `Version 0.13.3`
- Description copied from the changelog.
Once created this release will be automatically uploaded to PyPI.
If something goes wrong with the PyPI job the release can be published using the
`scripts/publish` script.
## Development proxy setup
To test and debug requests via a proxy it's best to run a proxy server locally.
Any server should do but [`mitmproxy`](https://mitmproxy.org/) is a good
choice - it's written in Python, fully featured and has excellent UI and
tools for introspection of requests.
You can install `mitmproxy` using `pip install mitmproxy` or [several
other ways](https://docs.mitmproxy.org/stable/overview-installation/).
`mitmproxy` does require setting up local TLS certificates for HTTPS requests,
as its main purpose is to allow developers to inspect requests that pass through
it. We can set them up follows:
1. [`pip install trustme-cli`](https://github.com/sethmlarson/trustme-cli/).
2. `trustme-cli -i example.org www.example.org`, assuming you want to test
connecting to that domain, this will create three files: `server.pem`,
`server.key` and `client.pem`.
3. `mitmproxy` requires a PEM file that includes the private key and the
certificate so we need to concatenate them:
`cat server.key server.pem > server.withkey.pem`.
4. Start the proxy server `mitmproxy --certs server.withkey.pem`, or use the
[other mitmproxy commands](https://docs.mitmproxy.org/stable/) with different
UI options.
At this point the server is ready to start serving requests, you'll need to
configure HTTPX2 as described in the
[proxy section](https://pydantic.dev/docs/httpx2/advanced/proxies/#http-proxies) and
the [SSL certificates section](https://pydantic.dev/docs/httpx2/advanced/ssl/),
this is where our previously generated `client.pem` comes in:
```python
import ssl
import httpx2
ctx = ssl.create_default_context(cafile="/path/to/client.pem")
with httpx2.Client(proxy="http://127.0.0.1:8080/", verify=ctx) as client:
response = client.get("https://example.org")
print(response.status_code) # should print 200
```
Note, however, that HTTPS requests will only succeed to the host specified
in the SSL/TLS certificate we generated, HTTPS requests to other hosts will
raise an error like:
```
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate
verify failed: Hostname mismatch, certificate is not valid for
'duckduckgo.com'. (_ssl.c:1108)
```
If you want to make requests to more hosts you'll need to regenerate the
certificates and include all the hosts you intend to connect to in the
seconds step, i.e.
`trustme-cli -i example.org www.example.org duckduckgo.com www.duckduckgo.com`
pydantic-httpx2-6dd59b9/docs/css/ 0000775 0000000 0000000 00000000000 15265474634 0016744 5 ustar 00root root 0000000 0000000 pydantic-httpx2-6dd59b9/docs/css/custom.css 0000664 0000000 0000000 00000000273 15265474634 0020772 0 ustar 00root root 0000000 0000000 div.autodoc-docstring {
padding-left: 20px;
margin-bottom: 30px;
border-left: 5px solid rgba(230, 230, 230);
}
div.autodoc-members {
padding-left: 20px;
margin-bottom: 15px;
}
pydantic-httpx2-6dd59b9/docs/environment_variables.md 0000664 0000000 0000000 00000005453 15265474634 0023101 0 ustar 00root root 0000000 0000000 # Environment Variables
The HTTPX library can be configured via environment variables.
Environment variables are used by default. To ignore environment variables, `trust_env` has to be set `False`. There are two ways to set `trust_env` to disable environment variables:
* On the client via `httpx2.Client(trust_env=False)`.
* Using the top-level API, such as `httpx2.get("", trust_env=False)`.
Here is a list of environment variables that HTTPX recognizes and what function they serve:
## Proxies
The environment variables documented below are used as a convention by various HTTP tooling, including:
* [cURL](https://github.com/curl/curl/blob/master/docs/MANUAL.md#environment-variables)
* [requests](https://github.com/psf/requests/blob/master/docs/user/advanced.rst#proxies)
For more information on using proxies in HTTPX, see [HTTP Proxies](advanced/proxies.md#http-proxies).
### `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`
Valid values: A URL to a proxy
`HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY` set the proxy to be used for `http`, `https`, or all requests respectively.
```bash
export HTTP_PROXY=http://my-external-proxy.com:1234
# This request will be sent through the proxy
python -c "import httpx2; httpx2.get('http://example.com')"
# This request will be sent directly, as we set `trust_env=False`
python -c "import httpx2; httpx2.get('http://example.com', trust_env=False)"
```
### `NO_PROXY`
Valid values: a comma-separated list of hostnames/urls
`NO_PROXY` disables the proxy for specific urls
```bash
export HTTP_PROXY=http://my-external-proxy.com:1234
export NO_PROXY=http://127.0.0.1,pydantic.dev
# As in the previous example, this request will be sent through the proxy
python -c "import httpx2; httpx2.get('http://example.com')"
# These requests will be sent directly, bypassing the proxy
python -c "import httpx2; httpx2.get('http://127.0.0.1:5000/my-api')"
python -c "import httpx2; httpx2.get('https://pydantic.dev/docs/httpx2/')"
```
## `SSL_CERT_FILE`
Valid values: a filename
If this environment variable is set then HTTPX will load
CA certificate from the specified file instead of the default
location.
Example:
```console
SSL_CERT_FILE=/path/to/ca-certs/ca-bundle.crt python -c "import httpx2; httpx2.get('https://example.com')"
```
## `SSL_CERT_DIR`
Valid values: a directory following an [OpenSSL specific layout](https://www.openssl.org/docs/manmaster/man3/SSL_CTX_load_verify_locations.html).
If this environment variable is set and the directory follows an [OpenSSL specific layout](https://www.openssl.org/docs/manmaster/man3/SSL_CTX_load_verify_locations.html) (ie. you ran `c_rehash`) then HTTPX will load CA certificates from this directory instead of the default location.
Example:
```console
SSL_CERT_DIR=/path/to/ca-certs/ python -c "import httpx2; httpx2.get('https://example.com')"
```
pydantic-httpx2-6dd59b9/docs/exceptions.md 0000664 0000000 0000000 00000003310 15265474634 0020654 0 ustar 00root root 0000000 0000000 # Exceptions
This page lists exceptions that may be raised when using HTTPX.
For an overview of how to work with HTTPX exceptions, see [Exceptions (Quickstart)](quickstart.md#exceptions).
## The exception hierarchy
* HTTPError
* RequestError
* TransportError
* TimeoutException
* ConnectTimeout
* ReadTimeout
* WriteTimeout
* PoolTimeout
* NetworkError
* ConnectError
* ReadError
* WriteError
* CloseError
* ProtocolError
* LocalProtocolError
* RemoteProtocolError
* ProxyError
* UnsupportedProtocol
* SSEError
* DecodingError
* TooManyRedirects
* HTTPStatusError
* InvalidURL
* CookieConflict
* StreamError
* StreamConsumed
* ResponseNotRead
* RequestNotRead
* StreamClosed
---
## Exception classes
::: httpx2.HTTPError
::: httpx2.RequestError
::: httpx2.TransportError
::: httpx2.TimeoutException
::: httpx2.ConnectTimeout
::: httpx2.ReadTimeout
::: httpx2.WriteTimeout
::: httpx2.PoolTimeout
::: httpx2.NetworkError
::: httpx2.ConnectError
::: httpx2.ReadError
::: httpx2.WriteError
::: httpx2.CloseError
::: httpx2.ProtocolError
::: httpx2.LocalProtocolError
::: httpx2.RemoteProtocolError
::: httpx2.ProxyError
::: httpx2.UnsupportedProtocol
::: httpx2.SSEError
::: httpx2.DecodingError
::: httpx2.TooManyRedirects
::: httpx2.HTTPStatusError
::: httpx2.InvalidURL
::: httpx2.CookieConflict
::: httpx2.StreamError
::: httpx2.StreamConsumed
::: httpx2.StreamClosed
::: httpx2.ResponseNotRead
::: httpx2.RequestNotRead
pydantic-httpx2-6dd59b9/docs/http2.md 0000664 0000000 0000000 00000005020 15265474634 0017534 0 ustar 00root root 0000000 0000000 # HTTP/2
HTTP/2 is a major new iteration of the HTTP protocol, that provides a far more
efficient transport, with potential performance benefits. HTTP/2 does not change
the core semantics of the request or response, but alters the way that data is
sent to and from the server.
Rather than the text format that HTTP/1.1 uses, HTTP/2 is a binary format.
The binary format provides full request and response multiplexing, and efficient
compression of HTTP headers. The stream multiplexing means that where HTTP/1.1
requires one TCP stream for each concurrent request, HTTP/2 allows a single TCP
stream to handle multiple concurrent requests.
HTTP/2 also provides support for functionality such as response prioritization,
and server push.
For a comprehensive guide to HTTP/2 you may want to check out "[http2 explained](https://http2-explained.haxx.se/)".
## Enabling HTTP/2
When using the `httpx2` client, HTTP/2 support is not enabled by default, because
HTTP/1.1 is a mature, battle-hardened transport layer, and our HTTP/1.1
implementation may be considered the more robust option at this point in time.
It is possible that a future version of `httpx2` may enable HTTP/2 support by default.
If you're issuing highly concurrent requests you might want to consider
trying out our HTTP/2 support. You can do so by first making sure to install
the optional HTTP/2 dependencies...
```shell
pip install 'httpx2[http2]'
```
And then instantiating a client with HTTP/2 support enabled:
```python
client = httpx2.AsyncClient(http2=True)
...
```
You can also instantiate a client as a context manager, to ensure that all
HTTP connections are nicely scoped, and will be closed once the context block
is exited.
```python
async with httpx2.AsyncClient(http2=True) as client:
...
```
HTTP/2 support is available on both `Client` and `AsyncClient`, although it's
typically more useful in async contexts if you're issuing lots of concurrent
requests.
## Inspecting the HTTP version
Enabling HTTP/2 support on the client does not *necessarily* mean that your
requests and responses will be transported over HTTP/2, since both the client
*and* the server need to support HTTP/2. If you connect to a server that only
supports HTTP/1.1 the client will use a standard HTTP/1.1 connection instead.
You can determine which version of the HTTP protocol was used by examining
the `.http_version` property on the response.
```python
client = httpx2.AsyncClient(http2=True)
response = await client.get(...)
print(response.http_version) # "HTTP/1.0", "HTTP/1.1", or "HTTP/2".
```
pydantic-httpx2-6dd59b9/docs/img/ 0000775 0000000 0000000 00000000000 15265474634 0016730 5 ustar 00root root 0000000 0000000 pydantic-httpx2-6dd59b9/docs/img/butterfly.png 0000664 0000000 0000000 00002467402 15265474634 0021475 0 ustar 00root root 0000000 0000000 PNG
IHDR ฃ MJ ViCCPICC Profile (c``RI,(ศaa``ศอ+)
rwRR`ศภผb
ษล>@%0|ปฦภข/๋ฬ:%5ตIต^ภืbฆ๐ีฏD0ีฃ ฎิโd �SJSlๅ๒ปศ):
ศbงCุ@์$๛XMH3}ศVHฮHม๘ศึIBOGbCํnฬโฤJ
cฎ%คVh็�สขฬ๔G`(ฅ*xๆ%๋้(30ยข๚s 8,ลฮ ฤ๏30ุ๎����nื~@\;b�'v$%
)-แำr�Hแ@=ัลiฦF`yF'ึ{��Vc``ฬภ๐wย��ฟ���w1P๓y !e๎ด๙้ pHYs %iTXtXML:com.adobe.xmp
2
0
72
1
72
1038
1
675
2020-11-04T11:11:96
Pixelmator 3.9
B�Wฆ @ IDATx์�`\ลต๐๑ู�ด*d[๎VฏVตไ"๗ฦุธaภ๔J ๓A" !HBy <ยL
9`\qฌ.ญถ฿๏\๑xฯI่ฅ�ีฃ-w็ฮ�๎ju็�33JqC @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @เ(ดบ:ฆี�4M3E1ผ@ ฃเะQ๑๑f@ @ ๗$P`๘ีฏป�{ิฺ๎์wฟ฿P[๛เเ3:{o ภ8๘b^E @ ๚T@�เ๊s�น�|เ,ซลฌ�มP๓^xโ-ฉศ฿๚ด2์@ 0ข zะเ๖๋ฏศ���|ggทof0*อhQซ5็t#ฃิ@` 8hG๖" @ฤ
�ไบK3๎�อ๎ํ๊๊fฒฺัdU&ณ,๗ฯด3b+Nล@ bZภAL^ ั" 5~เ~๔ฉ็ฎ์์์a2�มdVFณY%นใ:ฒG๘EBัยถhi๕D -ๆ8ญใIk@ @
ด;ํNXqฦกฦuFษ0!สh0+ปอฎ
�ห~๏?p6*# ฤ 84@ ขY@3\vรฺ�๛^fฒ:l%&2ษฟA๑๎฿ฏ9แ๘wEsฉ; @t0T!บตG @ (ุ๚์#I_yํ�2
3๕k:dุTRผ๋๏s&U]3{อขผT@ สQ^ช Dญฆmตฮฐ๚[{5�ะlฑฒีjU๑NW�ฤข�3~๘�x!
Q{xฉ8 3UCIC@ @ ชd้ลฯ^พ๗ะแ3ฬfIำยสl6)รั?v๔w%h๐
Aจ:ขTY*ฤ์กฅa ษ�ท<ฟ๙ลฟ]า9e
8งKฅ$ฤ�๒�๒ธ
B\๊ 0pศ88ว" @hตตๆEW^น�๏.ทศ|JU~บ๖�^4gฺญฑcฝRUช "ใ ะฒิขแ๖�~x์๎�{/4
l2ฏรnoฬ9โ{g\qํพ>ฌปB @เKศ8๘R"6@ @ zQ qณ๋7=y l*1ห ุ,-9>๎�]wร๋2DA๋ลฝQ G-@เเจ ) @ ๘jถษtโ�๏ฮkํ๐ฌ2�์๒&ข`ถจD๓ๅีs�cศส๒}ตุ
@ พ`จB฿Yณ'@ เ฿นsศึ๗?บT]tห0e4ัจ5f๑�UW\qp๓ะ|@ ใ BีB @ ุะ๊ถMZ~แฺม8ัๆpHภภค�qNZjยM็�gอ,ฝ[วึ ฑ$@ฦA,Mฺ |E}r6mฯซ๙iฆฏ๘66C o( �ฮ]๙ฃr[ฺฯ2-=hเtุTZrย�<๕เ=�7,ท! pฬศ88ๆฤ์ 4อpใUซฒ|ฒ๖;มฐม>z๔kตO>๘ะลซdผ5ฒEฺแข>ฑ!p่็m~ํฃูi4ษdTn�ีใf�j0๓ฤF#i @ฌ
qซGv! +`ะบณ�ษCื9โึ||เ๐Oo�ษO๎~่ฎF(ฅ>๗mผ ฿H@ซซณฬ:�ไี฿Zณลฆฬfณw9๗gพๆดKผ๗ส@ ่CาS๛]! "ฐ๚j�พ}-Kบ<�xษ20๘�ํ;wน๙-/=�็;"ฅิXh3ทๅly๓ร;$h0ิ$Aรๆ-ศuษ<๚'ๆ5
#L@ ุ ใ ๖1-D �Mเ[งฏ�็๚ซIRฆม๒๙ผ๊ps๓ฒ฿�๖5ZำkLเ ฝ! ํูใxi๓฿ฮ
BูFIูlV๐�ิ๑ธ
ฝฑส@ @เX88ึย D aศ\OF๚๐ป์6๋Aี.KยYT0VZO>๗ย๋ึkฺVkV*!UVk^ดvูึ6ฯZซ�ฉNป�๛ซNYwUj๎R2{ข๊hRY@``8ุวึ#ภ@0ดkn�๎k้#�๏+ กPP
dzวg�๎7}็ฎ
%ณภTฺ@o�v�ฆQํ�Wcฑ�ํvปsพp�๊+'ฏ?ทก7สง@ ๚JภA_Iณ@ ย,฿๊ีwฆ
N๙ร'3%Vํํํ#^�๋uJฐ*SขF@ำvฺ๓ศฯาฑิ้S�ณฝ*?cใY฿นyGิ4" ๐?๘( X`๊ษWฌศ]็๘Xฦ[+e4๖dhhXxว�qถฆี�0MGเ hf\ทx�ไถฮฎSํNง2Mฃง^�{ฺ"ฟg,y๚Ty @
8่O}๖ ๔ปAป`อท฿)ฮqS|ซ],v%ฆv์�ึ%g_5[๏๕{5ฉ Q$๐ท�พ+้ท฿ฟาbฑ
sฺฝcา�๐ะ.yษฃ่ RU@ @ ๘gmฯ&วใg^[U.-ศิฒGheฺ ฅ[~rลลyJ1฿ม?๑ฯะพํ*ษsSฦจด`qaถx๖[ด�uฮฯ�g@ ฎ"Eวqข ภ10\ูฝnอ์ปโrIjตษhRWตwด=๛๒K?x�๙_:ฆ pb@@ซฏทMฟ๊�.�าธ๘�i๒ดๅKn1ซ๐ฤ@๓h ภ p0>MG 8nๅ�;<กผ๘JWsK|BฒXส็๓ฉๆ%ื�v�Y2d%ใ>Gศ๏๙ธณฯูpธ๕2หmNpู[9ท๚ฮนฌ๑อธ Dท>Aอ*fWWผ9พ8W+สฅeืJ๓ว:uๅใ๕u้ฃป
ิ�ุคiฆีหfWIKฺQฅMX๑สฏnผr�>ฑคi๏sS" ๔ท<ธโย๕3&พY^ญHเ #MXป}ใy๋&ั๊๏#ฤ�#I@�}ธ๙[kฒฒ
z%{๔0mr๙ธฯ^ทdพ�{I๕ค. @ฏ
่ซ%x0ญช๔Mษ6ะ
ณk%yฺ้ิสงvพYุซ;ฃ0ขX@ซ-พ8sิ๏ฒวิส๒บtมb๙�!3')UG @ @ ฏ( ฎนp}i๕๘ยืห
๕Fkใrฦ๚LฏผYโฟ""ลดถ็UวqูWe*Wะนrแ์
Vg้Fำ8@ @ @เHMซ1~๛[kส+หr๋rวj๙รดโฌ]็oXนRฆ#ทๅ>I@ซฺ6sr้99้ฃฺ%hX<งzฃฆี�mE @ @ z๔ฬห/^WVUV๘Vฑ
2ห8๎ยทฎ<๗ryษ฿๘8�wb๕Ysฒฦ?T\ญ-9้vํเ�ฎA@ @ @ O๔า'/6ฉผh[Q๖(๏ S>ฑ๔ลM๗�pิง�๐ หพsูcณG~]_Aa
�y้ทIกํด@ @ ๘B้0ึธld|\Z%๓ัๆN.ฟ
๔์/dใลx๔{็ง|"?k6oๆฤ;๊๋ฑ&า@ @ @เH๐ภผfู�ฆTช*ษืWZhZยข๔็ฟyฉผ่h�เewy^๖}Eนูแ9Sซ๎�๚์/EGอฉ% ะzเ์uวฏฅ๕ฬชq9ป.