From f257dec93f9401646ac4812f8e8cc232adc13901 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 16 Jun 2025 17:22:35 +0100 Subject: [PATCH 1/5] Update README, homepage, and quickstart docs --- README.md | 80 ++++++++++++++++++---------------------- docs/clients.md | 2 +- docs/index.md | 91 +++++++++++++++------------------------------- docs/quickstart.md | 44 ++++++++++------------ docs/servers.md | 24 ++++++++++++ scripts/docs | 1 + 6 files changed, 111 insertions(+), 131 deletions(-) create mode 100644 docs/servers.md diff --git a/README.md b/README.md index 487a53a..047724b 100644 --- a/README.md +++ b/README.md @@ -6,40 +6,22 @@ --- -*The following is a [design proposal](https://www.encode.io/httpnext/) and is not yet fully functional. The work is well underway, tho be aware that some parts of the codebase are still under development.* - -# Background - -One of the core design principles informing `httpx` has been to aim to reduce the complexity of the stack. - -We've been trying to handle that incrementally, working from a requests-compatible API gradually introducing deprecations. This process creates a huge drag on being able to move the codebase towards where we'd actually like it to be, and introduces significant churn for our users. - -This work presents a significantly simplified implementation of `httpx`. - -* Seriously, a [radically simplified implementation](https://github.com/encode/httpnext/blob/main/src/httpx/_client.py). While still fulfiling the same set of functionality. -* A consistent & tightly typed set of HTTP components, with immutability throughout. Includes URLs, Query Parameters, Headers, Form & File interfaces, all of which are suitable for either client side or server side codebases. -* A re-engineered [connection pool implementation](https://github.com/encode/httpnext/blob/main/src/httpx/_pool.py), with tighter more obvious concurrency handling. -* The core networking component is simple enough to be directly included. There is no `httpx`/`httpcore` split, and the only hard dependencies here are `h11` and `truststore`. -* Seperately namespaced packages for `ahttpx` and `httpx`. - -There is also preliminary work ongoing for httpx *for both client-side and server-side usage*. +*The following is a [design proposal](https://www.encode.io/httpnext/) and is not yet complete. The work is well underway, tho be aware that some parts of the codebase are still under development.* --- -# Overview +A complete HTTP framework for Python. -Installation... +*Installation...* ```shell $ pip install git+https://github.com/encode/httpnext.git ``` -Lets get to work... +*Making requests as a client...* ```python ->>> import httpx ->>> cli = httpx.open_client() ->>> r = cli.get('https://www.example.org/') +>>> r = httpx.get('https://www.example.org/') >>> r >>> r.status_code @@ -50,12 +32,37 @@ Lets get to work... '\n\n\nExample Domain...' ``` +*Serving responses as the server...* + +```python +>>> def hello_world(request): +... content = httpx.HTML('hello, world.') +... return httpx.Response(code=200, content=content) +... +>>> with httpx.serve_http(hello_world) as server: +... print(f"Serving on {server.url} (Press CTRL+C to quit)") +... server.wait() +Serving on http://127.0.0.1:8080/ (Press CTRL+C to quit) +``` + +--- + +Features include... + +* Available in either sync or async flavours. +* A comprehensive set of HTTP components, with immutability throughout. +* A low complexity stack, with no required dependencies. +* Type annotation throughout. + +--- + # Documentation The httpx 1.0 [design proposal](https://www.encode.io/httpnext/) is now available. * [Quickstart](docs/quickstart.md) * [Clients](docs/clients.md) +* [Servers](docs/servers.md) * [Requests](docs/requests.md) * [Responses](docs/responses.md) * [URLs](docs/urls.md) @@ -65,18 +72,6 @@ The httpx 1.0 [design proposal](https://www.encode.io/httpnext/) is now availabl * [Low Level Networking](docs/networking.md) * [About](docs/about.md) -*Documentation & design work on `httpx` for server-side usage is in progress.* - ---- - -# Dependencies - -Package and dependencies... - -* httpx -* h11 -* truststore - --- # Collaboration @@ -85,19 +80,16 @@ The design repository for this work is currently private. We are looking towards --- -# Bringing this to life +## Background -In order to adequately address this space we need support & funding. +If you've been working with 0.x versions of HTTPX you'll notice significant API differences. -Ideally we'd be in a position financially where we're able to reasonably staff a minimal team of designers & developers. We will not be offering equity or sponsorship placements, but are instead seeking forward-looking investment that recognises the value of the infrastructure development on it's own merit. +Version 1.0 provides a much more tightly constrained API. It has a lighter installation footprint, far more obvious type annotations, and a lower overall complexity. -Our credentials to date include authorship of signifcant parts of the Python development ecosystem... +For example: -* Django REST framework. -* MkDocs. -* Uvicorn. -* Starlette. -* HTTPX. +* Client code [before](https://github.com/encode/httpx/blob/master/httpx/_client.py) and [after](https://github.com/encode/httpnext/blob/dev/src/httpx/_client.py). +* Response code [before](https://github.com/encode/httpx/blob/master/httpx/_models.py#L515) and [after](https://github.com/encode/httpnext/blob/dev/src/httpx/_response.py). --- diff --git a/docs/clients.md b/docs/clients.md index 48bdbcc..5e69c9a 100644 --- a/docs/clients.md +++ b/docs/clients.md @@ -189,5 +189,5 @@ You can expand on this pattern to provide behavior such as request or response s --- ← [Quickstart](quickstart.md) -[Requests](requests.md) → +[Servers](servers.md) →   diff --git a/docs/index.md b/docs/index.md index 6e49bd7..c22507b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,44 +10,21 @@ --- -# Background +A complete HTTP framework for Python. -One of the core design principles informing `httpx` has been to aim to reduce the complexity of the stack. - -We've been trying to handle that incrementally, working from a requests-compatible API gradually introducing deprecations. This process creates a huge drag on being able to move the codebase towards where we'd actually like it to be, and introduces significant churn for our users. - -This work insteads presents a significantly simplified implementation of `httpx`. - -* A consistent & tightly typed set of HTTP components, with immutability throughout. Includes URLs, Query Parameters, Headers, Form & File interfaces, all of which are suitable for either client side or server side codebases. -* Preliminary work for httpx to support both client-side and server-side usages. - -* A re-engineered [connection pool implementation](https://github.com/encode/httpx-insiders/blob/main/src/httpx/_pool.py), with tighter more obvious concurrency handling. -* The core networking component is simple enough to be directly included. The only hard dependencies here are `h11` and `truststore`. -* Seperately namespaced packages for `ahttpx` and `httpx`. - -There is also preliminary work ongoing for httpx *for both client-side and server-side usage*. - ---- - -# Overview - -Installation... +*Installation...* ```shell $ pip install git+https://github.com/encode/httpnext.git ``` -Lets get to work... +*Making requests as a client...* ```python ->>> import httpx ->>> ->>> with httpx.open_client() as client: -... r = client.get('https://www.example.org/') -... +>>> r = httpx.get('https://www.example.org/') >>> r ->>> r.code +>>> r.status_code 200 >>> r.headers['content-type'] 'text/html; charset=UTF-8' @@ -55,11 +32,9 @@ Lets get to work... '\n\n\nExample Domain...' ``` -We can also handle the server side... +*Serving responses as the server...* ```python ->>> import httpx ->>> >>> def hello_world(request): ... content = httpx.HTML('hello, world.') ... return httpx.Response(code=200, content=content) @@ -67,38 +42,35 @@ We can also handle the server side... >>> with httpx.serve_http(hello_world) as server: ... print(f"Serving on {server.url} (Press CTRL+C to quit)") ... server.wait() -... Serving on http://127.0.0.1:8080/ (Press CTRL+C to quit) ``` --- -# Documentation - -The httpx 1.0 [design proposal](https://www.encode.io/httpnext/) is now available. - -* [Quickstart](quickstart.md) -* [Clients](clients.md) -* [Requests](requests.md) -* [Responses](responses.md) -* [URLs](urls.md) -* [Headers](headers.md) -* [Content Types](content-types.md) -* [Connections](connections.md) -* [Low Level Networking](networking.md) -* [About](about.md) +Features include... -*Documentation & design work on `httpx` for server-side usage is in progress.* +* Available in either sync or async flavours. +* A comprehensive set of HTTP components, with immutability throughout. +* A low complexity stack, with no required dependencies. +* Type annotation throughout. --- -# Dependencies +# Documentation -Package and dependencies... +The httpx 1.0 [design proposal](https://www.encode.io/httpnext/) is now available. -* httpx -* h11 -* truststore +* [Quickstart](docs/quickstart.md) +* [Clients](docs/clients.md) +* [Servers](docs/servers.md) +* [Requests](docs/requests.md) +* [Responses](docs/responses.md) +* [URLs](docs/urls.md) +* [Headers](docs/headers.md) +* [Content Types](docs/content-types.md) +* [Connections](docs/connections.md) +* [Low Level Networking](docs/networking.md) +* [About](docs/about.md) --- @@ -108,19 +80,16 @@ The design repository for this work is currently private. We are looking towards --- -# Bringing this to life +## Background -In order to adequately address this space we need support & funding. +If you've been working with 0.x versions of HTTPX you'll notice significant API differences. -Ideally we'd be in a position financially where we're able to reasonably staff a minimal team of designers & developers. We will not be offering equity or sponsorship placements, but are instead seeking forward-looking investment that recognises the value of the infrastructure development on it's own merit. +Version 1.0 provides a much more tightly constrained API. It has a lighter installation footprint, far more obvious type annotations, and a lower overall complexity. -Our credentials to date include authorship of signifcant parts of the Python development ecosystem... +For example: -* Django REST framework. -* MkDocs. -* Uvicorn. -* Starlette. -* HTTPX. +* Client code [before](https://github.com/encode/httpx/blob/master/httpx/_client.py) and [after](https://github.com/encode/httpnext/blob/dev/src/httpx/_client.py). +* Response code [before](https://github.com/encode/httpx/blob/master/httpx/_models.py#L515) and [after](https://github.com/encode/httpnext/blob/dev/src/httpx/_response.py). --- diff --git a/docs/quickstart.md b/docs/quickstart.md index ec829a5..cf4a18b 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -12,16 +12,10 @@ First, start by importing `httpx`... >>> import httpx ``` -To send requests we'll need a client... - -```python ->>> cli = httpx.open_client() -``` - Now, let’s try to get a webpage. ```python ->>> r = cli.get('https://httpbin.org/get') +>>> r = httpx.get('https://httpbin.org/get') >>> r ``` @@ -30,15 +24,15 @@ To make an HTTP `POST` request, including some content... ```python >>> form = httpx.Form({'key': 'value'}) ->>> r = cli.post('https://httpbin.org/post', content=form) +>>> r = httpx.post('https://httpbin.org/post', content=form) ``` Shortcut methods for `PUT`, `PATCH`, and `DELETE` requests follow the same style... ```python ->>> r = cli.put('https://httpbin.org/put', content=form) ->>> r = cli.patch('https://httpbin.org/patch', content=form) ->>> r = cli.delete('https://httpbin.org/delete') +>>> r = httpx.put('https://httpbin.org/put', content=form) +>>> r = httpx.patch('https://httpbin.org/patch', content=form) +>>> r = httpx.delete('https://httpbin.org/delete') ``` ## Passing Parameters in URLs @@ -48,7 +42,7 @@ To include URL query parameters in the request, construct a URL using the `param ```python >>> params = {'key1': 'value1', 'key2': 'value2'} >>> url = httpx.URL('https://httpbin.org/get', params=params) ->>> r = cli.get(url) +>>> r = httpx.get(url) ``` You can also pass a list of items as a value... @@ -56,7 +50,7 @@ You can also pass a list of items as a value... ```python >>> params = {'key1': 'value1', 'key2': ['value2', 'value3']} >>> url = httpx.URL('https://httpbin.org/get', params=params) ->>> r = cli.get(url) +>>> r = httpx.get(url) ``` ## Custom Headers @@ -66,7 +60,7 @@ To include additional headers in the outgoing request, use the `headers` keyword ```python >>> url = 'https://httpbin.org/headers' >>> headers = {'User-Agent': 'my-app/0.0.1'} ->>> r = cli.get(url, headers=headers) +>>> r = httpx.get(url, headers=headers) ``` --- @@ -76,7 +70,7 @@ To include additional headers in the outgoing request, use the `headers` keyword HTTPX will automatically handle decoding the response content into unicode text. ```python ->>> r = cli.get('https://www.example.org/') +>>> r = httpx.get('https://www.example.org/') >>> r.text '\n\n\nExample Domain...' ``` @@ -86,7 +80,7 @@ HTTPX will automatically handle decoding the response content into unicode text. The response content can also be accessed as bytes, for non-text responses. ```python ->>> r.content +>>> r.body b'\n\n\nExample Domain...' ``` @@ -95,7 +89,7 @@ b'\n\n\nExample Domain...' Often Web API responses will be encoded as JSON. ```python ->>> r = cli.get('https://httpbin.org/get') +>>> r = httpx.get('https://httpbin.org/get') >>> r.json() {'args': {}, 'headers': {'Host': 'httpbin.org', 'User-Agent': 'dev', 'X-Amzn-Trace-Id': 'Root=1-679814d5-0f3d46b26686f5013e117085'}, 'origin': '21.35.60.128', 'url': 'https://httpbin.org/get'} ``` @@ -108,7 +102,7 @@ Some types of HTTP requests, such as `POST` and `PUT` requests, can include data ```python >>> form = httpx.Form({'key1': 'value1', 'key2': 'value2'}) ->>> r = cli.post("https://httpbin.org/post", content=form) +>>> r = httpx.post("https://httpbin.org/post", content=form) >>> r.json() { ... @@ -124,7 +118,7 @@ Form encoded data can also include multiple values from a given key. ```python >>> form = httpx.Form({'key1': ['value1', 'value2']}) ->>> r = cli.post("https://httpbin.org/post", content=form) +>>> r = httpx.post("https://httpbin.org/post", content=form) >>> r.json() { ... @@ -144,7 +138,7 @@ You can also upload files, using HTTP multipart encoding. ```python >>> files = httpx.Files({'upload': httpx.File('uploads/report.xls')}) ->>> r = cli.post("https://httpbin.org/post", content=files) +>>> r = httpx.post("https://httpbin.org/post", content=files) >>> r.json() { ... @@ -161,7 +155,7 @@ If you need to include non-file data fields in the multipart form, use the `data >>> form = {'message': 'Hello, world!'} >>> files = {'upload': httpx.File('uploads/report.xls')} >>> data = httpx.MultiPart(form=form, files=files) ->>> r = cli.post("https://httpbin.org/post", content=data) +>>> r = httpx.post("https://httpbin.org/post", content=data) >>> r.json() { ... @@ -182,7 +176,7 @@ For more complicated data structures you'll often want to use JSON encoding inst ```python >>> data = {'integer': 123, 'boolean': True, 'list': ['a', 'b', 'c']} ->>> r = cli.post("https://httpbin.org/post", content=httpx.JSON(data)) +>>> r = httpx.post("https://httpbin.org/post", content=httpx.JSON(data)) >>> r.json() { ... @@ -206,7 +200,7 @@ either a `bytes` type or a generator that yields `bytes`. ```python >>> content = b'Hello, world' ->>> r = cli.post("https://httpbin.org/post", content=content) +>>> r = httpx.post("https://httpbin.org/post", content=content) ``` You may also want to set a custom `Content-Type` header when uploading @@ -219,7 +213,7 @@ binary data. We can inspect the HTTP status code of the response: ```python ->>> r = cli.get('https://httpbin.org/get') +>>> r = httpx.get('https://httpbin.org/get') >>> r.code 200 ``` @@ -259,7 +253,7 @@ For large downloads you may want to use streaming responses that do not load the You can stream the binary content of the response... ```python ->>> with cli.stream("GET", "https://www.example.com") as r: +>>> with httpx.stream("GET", "https://www.example.com") as r: ... for data in r.stream: ... print(data) ``` diff --git a/docs/servers.md b/docs/servers.md new file mode 100644 index 0000000..b952cf5 --- /dev/null +++ b/docs/servers.md @@ -0,0 +1,24 @@ +## `serve_http` + +```python +>>> def hello_world(request): +... content = httpx.HTML('hello, world.') +... return httpx.Response(code=200, content=content) +... +>>> with httpx.serve_http(hello_world) as server: +... print(f"Serving on {server.url} (Press CTRL+C to quit)") +... server.wait() +Serving on http://127.0.0.1:8080/ (Press CTRL+C to quit) +``` + +## HTTPServer + +* `.wait()` +* `.url` +* `.connections` + +--- + +← [Clients](clients.md) +[Requests](requests.md) → +  diff --git a/scripts/docs b/scripts/docs index d34124e..91a89b7 100755 --- a/scripts/docs +++ b/scripts/docs @@ -16,6 +16,7 @@ pages = { '/': 'docs/index.md', '/quickstart': 'docs/quickstart.md', '/clients': 'docs/clients.md', + '/servers': 'docs/servers.md', '/requests': 'docs/requests.md', '/responses': 'docs/responses.md', '/urls': 'docs/urls.md', From 89332fb5aabcf47dfc1268bc4e252fdbbf74df4d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 17 Jun 2025 11:45:25 +0100 Subject: [PATCH 2/5] Tweak docs --- docs/connections.md | 2 +- docs/index.md | 2 +- docs/requests.md | 2 +- docs/servers.md | 9 +++++++-- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/connections.md b/docs/connections.md index 234fb43..2e8e1dd 100644 --- a/docs/connections.md +++ b/docs/connections.md @@ -115,7 +115,7 @@ The `NetworkBackend` is responsible for managing the TCP stream, providing a raw ```python with httpx.open_connection("https://www.example.com/") as conn: - with conn.upgrade("GET", "/feed", {"Upgrade": "WebSocket") as stream: + with conn.upgrade("GET", "/feed", {"Upgrade": "WebSocket"}) as stream: ... ``` diff --git a/docs/index.md b/docs/index.md index c22507b..ab3c22e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -80,7 +80,7 @@ The design repository for this work is currently private. We are looking towards --- -## Background +# Background If you've been working with 0.x versions of HTTPX you'll notice significant API differences. diff --git a/docs/requests.md b/docs/requests.md index fd143d0..16f6ae3 100644 --- a/docs/requests.md +++ b/docs/requests.md @@ -95,6 +95,6 @@ Including direct file uploads... --- -← [Clients](clients.md) +← [Servers](servers.md) [Responses](responses.md) →   diff --git a/docs/servers.md b/docs/servers.md index b952cf5..54d503f 100644 --- a/docs/servers.md +++ b/docs/servers.md @@ -1,10 +1,15 @@ -## `serve_http` +# Servers + +The HTTP server provides a simple request/response API. +This gives you a lightweight way to build web applications or APIs. + +### `serve_http(endpoint, listeners=None)` ```python >>> def hello_world(request): ... content = httpx.HTML('hello, world.') ... return httpx.Response(code=200, content=content) -... + >>> with httpx.serve_http(hello_world) as server: ... print(f"Serving on {server.url} (Press CTRL+C to quit)") ... server.wait() From 77076e4d7ff85beba978c2b9f4185a17c497c494 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 17 Jun 2025 11:46:02 +0100 Subject: [PATCH 3/5] Tweak docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 047724b..b92e782 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ $ pip install git+https://github.com/encode/httpnext.git >>> def hello_world(request): ... content = httpx.HTML('hello, world.') ... return httpx.Response(code=200, content=content) -... + >>> with httpx.serve_http(hello_world) as server: ... print(f"Serving on {server.url} (Press CTRL+C to quit)") ... server.wait() From ba54d01e3d80e1637ff2b99709319d66fe7b07e6 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 17 Jun 2025 12:36:19 +0100 Subject: [PATCH 4/5] Use 'httpx.Client' and 'httpx.ConnectionPool' style --- docs/clients.md | 14 +++++------ docs/connections.md | 14 +++++------ docs/networking.md | 8 +++---- scripts/unasync | 1 + src/ahttpx/__init__.py | 10 +++++--- src/ahttpx/_client.py | 20 +++++----------- src/ahttpx/_pool.py | 22 ++++++------------ src/ahttpx/_quickstart.py | 49 +++++++++++++++++++++++++++++++++++++++ src/httpx/__init__.py | 10 +++++--- src/httpx/_client.py | 20 +++++----------- src/httpx/_pool.py | 22 ++++++------------ src/httpx/_quickstart.py | 49 +++++++++++++++++++++++++++++++++++++++ 12 files changed, 157 insertions(+), 82 deletions(-) create mode 100644 src/ahttpx/_quickstart.py create mode 100644 src/httpx/_quickstart.py diff --git a/docs/clients.md b/docs/clients.md index 5e69c9a..9693f13 100644 --- a/docs/clients.md +++ b/docs/clients.md @@ -3,7 +3,7 @@ HTTP requests are sent by using a `Client` instance. Client instances are thread safe interfaces that maintain a pool of HTTP connections. ```python ->>> cli = httpx.open_client() +>>> cli = httpx.Client() >>> cli ``` @@ -29,7 +29,7 @@ The connections in the pool can be explicitly closed, using the `close()` method Client instances support being used in a context managed scope. You can use this style to enforce properly scoped resources, ensuring that the connection pool is cleanly closed when no longer required. ```python ->>> with httpx.open_client() as cli: +>>> with httpx.Client() as cli: ... cli.get("https://www.example.com") ``` @@ -44,7 +44,7 @@ The recommened usage is to *either* a have single global instance created at imp Client instances can be configured with a base URL that is used when constructing requests... ```python ->>> cli = httpx.open_client(url="https://www.httpbin.org") +>>> cli = httpx.Client(url="https://www.httpbin.org") >>> r = cli.get("/json") >>> r @@ -67,7 +67,7 @@ You can override this behavior by explicitly specifying the default headers... ```python >>> headers = {"User-Agent": "dev", "Accept-Encoding": "gzip"} ->>> cli = httpx.open_client(headers=headers) +>>> cli = httpx.Client(headers=headers) >>> r = cli.get("https://www.example.com/") ``` @@ -81,8 +81,8 @@ The connection pool used by the client can be configured in order to customise t >>> no_verify.check_hostname = False >>> no_verify.verify_mode = ssl.CERT_NONE >>> # Instantiate a client with our custom SSL context. ->>> with httpx.open_connection_pool(ssl_context=no_verify) as pool: ->>> with httpx.open_client(transport=pool) as cli: +>>> with httpx.ConnectionPool(ssl_context=no_verify) as pool: +>>> with httpx.Client(transport=pool) as cli: >>> ... ``` @@ -120,7 +120,7 @@ class MockTransport(httpx.Transport): response = httpx.Response(200, content=httpx.Text('Hello, world')) transport = MockTransport(response=response) -cli = httpx.open_client(transport=transport) +cli = httpx.Client(transport=transport) print(cli.get('https://www.example.com')) ``` diff --git a/docs/connections.md b/docs/connections.md index 2e8e1dd..a442439 100644 --- a/docs/connections.md +++ b/docs/connections.md @@ -5,7 +5,7 @@ The mechanics of sending HTTP requests is dealt with by the `ConnectionPool` and We can introspect a `Client` instance to get some visibility onto the state of the connection pool. ```python ->>> with httpx.open_client() as cli +>>> with httpx.Client() as cli >>> urls = [ ... "https://www.wikipedia.org/", ... "https://www.theguardian.com/", @@ -32,7 +32,7 @@ The `Client` class is responsible for handling redirects and cookies. It also ensures that outgoing requests include a default set of headers such as `User-Agent` and `Accept-Encoding`. ```python -with httpx.open_client() as cli: +with httpx.Client() as cli: r = cli.request("GET", "https://www.example.com/") ``` @@ -41,7 +41,7 @@ The `Client` class sends requests using a `ConnectionPool`, which is responsible A single connection pool is able to handle multiple concurrent requests, with locking in place to ensure that the pool does not become over-saturated. ```python -with httpx.open_connection_pool() as pool: +with httpx.ConnectionPool() as pool: r = pool.request("GET", "https://www.example.com/") ``` @@ -69,7 +69,7 @@ The `NetworkBackend` is responsible for managing the TCP stream, providing a raw ### `.request(method, url, headers=None, content=None)` ```python ->>> with httpx.open_connection_pool() as pool: +>>> with httpx.ConnectionPool() as pool: >>> res = pool.request("GET", "https://www.example.com") >>> res, pool , @@ -78,7 +78,7 @@ The `NetworkBackend` is responsible for managing the TCP stream, providing a raw ### `.stream(method, url, headers=None, content=None)` ```python ->>> with httpx.open_connection_pool() as pool: +>>> with httpx.ConnectionPool() as pool: >>> with pool.stream("GET", "https://www.example.com") as res: >>> res, pool , @@ -87,7 +87,7 @@ The `NetworkBackend` is responsible for managing the TCP stream, providing a raw ### `.send(request)` ```python ->>> with httpx.open_connection_pool() as pool: +>>> with httpx.ConnectionPool() as pool: >>> req = httpx.Request("GET", "https://www.example.com") >>> with pool.send(req) as res: >>> res.read() @@ -98,7 +98,7 @@ The `NetworkBackend` is responsible for managing the TCP stream, providing a raw ### `.close()` ```python ->>> with httpx.open_connection_pool() as pool: +>>> with httpx.ConnectionPool() as pool: >>> pool.close() ``` diff --git a/docs/networking.md b/docs/networking.md index aa7c88f..0be863d 100644 --- a/docs/networking.md +++ b/docs/networking.md @@ -96,7 +96,7 @@ The timeout context manager can be used to wrap socket operations anywhere in th Here's an example of enforcing an overall 3 second timeout on a request. ```python ->>> with httpx.open_client() as cli: +>>> with httpx.Client() as cli: >>> with httpx.timeout(3.0): >>> res = cli.get('https://www.example.com') >>> print(res) @@ -109,7 +109,7 @@ Timeout contexts provide an API allowing for deadlines to be cancelled. In this example we enforce a 3 second timeout on *receiving the start of* a streaming HTTP response... ```python ->>> with httpx.open_client() as cli: +>>> with httpx.Client() as cli: >>> with httpx.timeout(3.0) as t: >>> with cli.stream('https://www.example.com') as r: >>> t.cancel() @@ -222,8 +222,8 @@ class RecordingStream(NetworkStreamInterface): We can now instantiate a client using this network backend. ```python ->>> transport = httpx.open_connection_pool(backend=RecordingBackend()) ->>> cli = httpx.open_client(transport=transport) +>>> transport = httpx.ConnectionPool(backend=RecordingBackend()) +>>> cli = httpx.Client(transport=transport) >>> cli.get('https://www.example.com') ``` diff --git a/scripts/unasync b/scripts/unasync index 7fcac1a..8ae754e 100755 --- a/scripts/unasync +++ b/scripts/unasync @@ -8,6 +8,7 @@ unasync.unasync_files( "src/ahttpx/_content.py", "src/ahttpx/_headers.py", "src/ahttpx/_pool.py", + "src/ahttpx/_quickstart.py", "src/ahttpx/_response.py", "src/ahttpx/_request.py", "src/ahttpx/_streams.py", diff --git a/src/ahttpx/__init__.py b/src/ahttpx/__init__.py index 12d0fbf..e9b80d2 100644 --- a/src/ahttpx/__init__.py +++ b/src/ahttpx/__init__.py @@ -2,7 +2,8 @@ from ._content import * # Content, File, Files, Form, HTML, JSON, MultiPart, Text from ._headers import * # Headers from ._network import * # NetworkBackend, NetworkStream, timeout -from ._pool import * # Connection, ConnectionPool, Transport, open_connection_pool, open_connection +from ._pool import * # Connection, ConnectionPool, Transport +from ._quickstart import * # get, post, put, patch, delete from ._response import * # Response from ._request import * # Request from ._streams import * # ByteStream, IterByteStream, FileStream, Stream @@ -17,10 +18,12 @@ "Connection", "ConnectionPool", "Content", + "delete", "File", "FileStream", "Files", "Form", + "get", "Headers", "HTML", "IterByteStream", @@ -28,9 +31,10 @@ "MultiPart", "NetworkBackend", "NetworkStream", - "open_client", - "open_connection_pool", "open_connection", + "post", + "put", + "patch", "Response", "Request", "serve_http", diff --git a/src/ahttpx/_client.py b/src/ahttpx/_client.py index f202925..a7c1615 100644 --- a/src/ahttpx/_client.py +++ b/src/ahttpx/_client.py @@ -4,30 +4,32 @@ from ._content import Content from ._headers import Headers -from ._pool import Transport, open_connection_pool +from ._pool import ConnectionPool, Transport from ._request import Request from ._response import Response from ._streams import Stream from ._urls import URL -__all__ = ["Client", "Content", "open_client"] +__all__ = ["Client", "Content"] class Client: def __init__( self, - transport: Transport, url: URL | str | None = None, headers: Headers | typing.Mapping[str, str] | None = None, + transport: Transport | None = None, ): if url is None: url = "" if headers is None: headers = {"User-Agent": "dev"} + if transport is None: + transport = ConnectionPool() - self.transport = transport self.url = URL(url) self.headers = Headers(headers) + self.transport = transport self.via = RedirectMiddleware(self.transport) def build_request( @@ -155,13 +157,3 @@ async def send(self, request: Request) -> typing.AsyncIterator[Response]: async def aclose(self): pass - - -async def open_client( - transport: Transport | None = None, - url: URL | str | None = None, - headers: Headers | typing.Mapping[str, str] | None = None, -): - if transport is None: - transport = await open_connection_pool() - return Client(transport=transport, url=url, headers=headers) diff --git a/src/ahttpx/_pool.py b/src/ahttpx/_pool.py index 41c79c6..e637921 100644 --- a/src/ahttpx/_pool.py +++ b/src/ahttpx/_pool.py @@ -19,7 +19,6 @@ "Transport", "ConnectionPool", "Connection", - "open_connection_pool", "open_connection", ] @@ -59,7 +58,13 @@ async def stream( class ConnectionPool(Transport): - def __init__(self, ssl_context: ssl.SSLContext, backend: NetworkBackend): + def __init__(self, ssl_context: ssl.SSLContext | None = None, backend: NetworkBackend | None = None): + if ssl_context is None: + import truststore + ssl_context = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + if backend is None: + backend = NetworkBackend() + self._connections: list[Connection] = [] self._ssl_context = ssl_context self._network_backend = backend @@ -151,19 +156,6 @@ async def __aexit__( await self.close() -async def open_connection_pool( - ssl_context: ssl.SSLContext | None = None, - backend: NetworkBackend | None = None -) -> ConnectionPool: - if ssl_context is None: - import truststore - ssl_context = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) - if backend is None: - backend = NetworkBackend() - - return ConnectionPool(ssl_context=ssl_context, backend=backend) - - class Connection(Transport): def __init__(self, stream: "NetworkStream", origin: URL | str): self._stream = stream diff --git a/src/ahttpx/_quickstart.py b/src/ahttpx/_quickstart.py new file mode 100644 index 0000000..8b6e12f --- /dev/null +++ b/src/ahttpx/_quickstart.py @@ -0,0 +1,49 @@ +import typing + +from ._client import Client +from ._content import Content +from ._headers import Headers +from ._streams import Stream +from ._urls import URL + + +__all__ = ['get', 'post', 'put', 'patch', 'delete'] + + +async def get( + url: URL | str, + headers: Headers | typing.Mapping[str, str] | None = None, +): + async with Client() as client: + return await client.request("GET", url=url, headers=headers) + +async def post( + url: URL | str, + headers: Headers | typing.Mapping[str, str] | None = None, + content: Content | Stream | bytes | None = None, +): + async with Client() as client: + return await client.request("POST", url, headers=headers, content=content) + +async def put( + url: URL | str, + headers: Headers | typing.Mapping[str, str] | None = None, + content: Content | Stream | bytes | None = None, +): + async with Client() as client: + return await client.request("PUT", url, headers=headers, content=content) + +async def patch( + url: URL | str, + headers: Headers | typing.Mapping[str, str] | None = None, + content: Content | Stream | bytes | None = None, +): + async with Client() as client: + return await client.request("PATCH", url, headers=headers, content=content) + +async def delete( + url: URL | str, + headers: Headers | typing.Mapping[str, str] | None = None, +): + async with Client() as client: + return await client.request("DELETE", url=url, headers=headers) diff --git a/src/httpx/__init__.py b/src/httpx/__init__.py index 12d0fbf..e9b80d2 100644 --- a/src/httpx/__init__.py +++ b/src/httpx/__init__.py @@ -2,7 +2,8 @@ from ._content import * # Content, File, Files, Form, HTML, JSON, MultiPart, Text from ._headers import * # Headers from ._network import * # NetworkBackend, NetworkStream, timeout -from ._pool import * # Connection, ConnectionPool, Transport, open_connection_pool, open_connection +from ._pool import * # Connection, ConnectionPool, Transport +from ._quickstart import * # get, post, put, patch, delete from ._response import * # Response from ._request import * # Request from ._streams import * # ByteStream, IterByteStream, FileStream, Stream @@ -17,10 +18,12 @@ "Connection", "ConnectionPool", "Content", + "delete", "File", "FileStream", "Files", "Form", + "get", "Headers", "HTML", "IterByteStream", @@ -28,9 +31,10 @@ "MultiPart", "NetworkBackend", "NetworkStream", - "open_client", - "open_connection_pool", "open_connection", + "post", + "put", + "patch", "Response", "Request", "serve_http", diff --git a/src/httpx/_client.py b/src/httpx/_client.py index 4c20f2b..d808ff1 100644 --- a/src/httpx/_client.py +++ b/src/httpx/_client.py @@ -4,30 +4,32 @@ from ._content import Content from ._headers import Headers -from ._pool import Transport, open_connection_pool +from ._pool import ConnectionPool, Transport from ._request import Request from ._response import Response from ._streams import Stream from ._urls import URL -__all__ = ["Client", "Content", "open_client"] +__all__ = ["Client", "Content"] class Client: def __init__( self, - transport: Transport, url: URL | str | None = None, headers: Headers | typing.Mapping[str, str] | None = None, + transport: Transport | None = None, ): if url is None: url = "" if headers is None: headers = {"User-Agent": "dev"} + if transport is None: + transport = ConnectionPool() - self.transport = transport self.url = URL(url) self.headers = Headers(headers) + self.transport = transport self.via = RedirectMiddleware(self.transport) def build_request( @@ -155,13 +157,3 @@ def send(self, request: Request) -> typing.Iterator[Response]: def aclose(self): pass - - -def open_client( - transport: Transport | None = None, - url: URL | str | None = None, - headers: Headers | typing.Mapping[str, str] | None = None, -): - if transport is None: - transport = open_connection_pool() - return Client(transport=transport, url=url, headers=headers) diff --git a/src/httpx/_pool.py b/src/httpx/_pool.py index 5c92eac..a814bee 100644 --- a/src/httpx/_pool.py +++ b/src/httpx/_pool.py @@ -19,7 +19,6 @@ "Transport", "ConnectionPool", "Connection", - "open_connection_pool", "open_connection", ] @@ -59,7 +58,13 @@ def stream( class ConnectionPool(Transport): - def __init__(self, ssl_context: ssl.SSLContext, backend: NetworkBackend): + def __init__(self, ssl_context: ssl.SSLContext | None = None, backend: NetworkBackend | None = None): + if ssl_context is None: + import truststore + ssl_context = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + if backend is None: + backend = NetworkBackend() + self._connections: list[Connection] = [] self._ssl_context = ssl_context self._network_backend = backend @@ -151,19 +156,6 @@ def __exit__( self.close() -def open_connection_pool( - ssl_context: ssl.SSLContext | None = None, - backend: NetworkBackend | None = None -) -> ConnectionPool: - if ssl_context is None: - import truststore - ssl_context = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) - if backend is None: - backend = NetworkBackend() - - return ConnectionPool(ssl_context=ssl_context, backend=backend) - - class Connection(Transport): def __init__(self, stream: "NetworkStream", origin: URL | str): self._stream = stream diff --git a/src/httpx/_quickstart.py b/src/httpx/_quickstart.py new file mode 100644 index 0000000..1a97530 --- /dev/null +++ b/src/httpx/_quickstart.py @@ -0,0 +1,49 @@ +import typing + +from ._client import Client +from ._content import Content +from ._headers import Headers +from ._streams import Stream +from ._urls import URL + + +__all__ = ['get', 'post', 'put', 'patch', 'delete'] + + +def get( + url: URL | str, + headers: Headers | typing.Mapping[str, str] | None = None, +): + with Client() as client: + return client.request("GET", url=url, headers=headers) + +def post( + url: URL | str, + headers: Headers | typing.Mapping[str, str] | None = None, + content: Content | Stream | bytes | None = None, +): + with Client() as client: + return client.request("POST", url, headers=headers, content=content) + +def put( + url: URL | str, + headers: Headers | typing.Mapping[str, str] | None = None, + content: Content | Stream | bytes | None = None, +): + with Client() as client: + return client.request("PUT", url, headers=headers, content=content) + +def patch( + url: URL | str, + headers: Headers | typing.Mapping[str, str] | None = None, + content: Content | Stream | bytes | None = None, +): + with Client() as client: + return client.request("PATCH", url, headers=headers, content=content) + +def delete( + url: URL | str, + headers: Headers | typing.Mapping[str, str] | None = None, +): + with Client() as client: + return client.request("DELETE", url=url, headers=headers) From cfe4543183a9ec720b1a237a71caad8427719d43 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 17 Jun 2025 12:40:24 +0100 Subject: [PATCH 5/5] Use 'httpx.Client' and 'httpx.ConnectionPool' style --- src/ahttpx/__init__.py | 2 +- src/httpx/__init__.py | 2 +- tests/test_client.py | 2 +- tests/test_pool.py | 10 ++--- tests/test_quickstart.py | 79 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 tests/test_quickstart.py diff --git a/src/ahttpx/__init__.py b/src/ahttpx/__init__.py index e9b80d2..532cf2c 100644 --- a/src/ahttpx/__init__.py +++ b/src/ahttpx/__init__.py @@ -1,4 +1,4 @@ -from ._client import * # Client, open_client +from ._client import * # Client from ._content import * # Content, File, Files, Form, HTML, JSON, MultiPart, Text from ._headers import * # Headers from ._network import * # NetworkBackend, NetworkStream, timeout diff --git a/src/httpx/__init__.py b/src/httpx/__init__.py index e9b80d2..532cf2c 100644 --- a/src/httpx/__init__.py +++ b/src/httpx/__init__.py @@ -1,4 +1,4 @@ -from ._client import * # Client, open_client +from ._client import * # Client from ._content import * # Content, File, Files, Form, HTML, JSON, MultiPart, Text from ._headers import * # Headers from ._network import * # NetworkBackend, NetworkStream, timeout diff --git a/tests/test_client.py b/tests/test_client.py index e89e27a..6c84e8d 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -17,7 +17,7 @@ def echo(request): @pytest.fixture def client(): - with httpx.open_client() as client: + with httpx.Client() as client: yield client diff --git a/tests/test_pool.py b/tests/test_pool.py index ae3020b..175c78c 100644 --- a/tests/test_pool.py +++ b/tests/test_pool.py @@ -14,7 +14,7 @@ def server(): def test_connection_pool_request(server): - with httpx.open_connection_pool() as pool: + with httpx.ConnectionPool() as pool: assert repr(pool) == "" assert len(pool.connections) == 0 @@ -26,7 +26,7 @@ def test_connection_pool_request(server): def test_connection_pool_connection_close(server): - with httpx.open_connection_pool() as pool: + with httpx.ConnectionPool() as pool: assert repr(pool) == "" assert len(pool.connections) == 0 @@ -38,7 +38,7 @@ def test_connection_pool_connection_close(server): def test_connection_pool_stream(server): - with httpx.open_connection_pool() as pool: + with httpx.ConnectionPool() as pool: assert repr(pool) == "" assert len(pool.connections) == 0 @@ -53,7 +53,7 @@ def test_connection_pool_stream(server): def test_connection_pool_cannot_request_after_closed(server): - with httpx.open_connection_pool() as pool: + with httpx.ConnectionPool() as pool: pool with pytest.raises(RuntimeError): @@ -61,7 +61,7 @@ def test_connection_pool_cannot_request_after_closed(server): def test_connection_pool_should_have_managed_lifespan(server): - pool = httpx.open_connection_pool() + pool = httpx.ConnectionPool() with pytest.warns(UserWarning): del pool diff --git a/tests/test_quickstart.py b/tests/test_quickstart.py new file mode 100644 index 0000000..123ae86 --- /dev/null +++ b/tests/test_quickstart.py @@ -0,0 +1,79 @@ +import json +import httpx +import pytest + + +def echo(request): + request.read() + body = None if not request.body else json.loads(request.body) + response = httpx.Response(200, content=httpx.JSON({ + 'method': request.method, + 'query-params': dict(request.url.params.items()), + 'content-type': request.headers.get('Content-Type'), + 'json': body, + })) + return response + + +@pytest.fixture +def server(): + with httpx.serve_http(echo) as server: + yield server + + +def test_get(server): + r = httpx.get(server.url) + assert r.code == 200 + assert json.loads(r.body) == { + 'method': 'GET', + 'query-params': {}, + 'content-type': None, + 'json': None, + } + + +def test_post(server): + data = httpx.JSON({"data": 123}) + r = httpx.post(server.url, content=data) + assert r.code == 200 + assert json.loads(r.body) == { + 'method': 'POST', + 'query-params': {}, + 'content-type': 'application/json', + 'json': {"data": 123}, + } + + +def test_put(server): + data = httpx.JSON({"data": 123}) + r = httpx.put(server.url, content=data) + assert r.code == 200 + assert json.loads(r.body) == { + 'method': 'PUT', + 'query-params': {}, + 'content-type': 'application/json', + 'json': {"data": 123}, + } + + +def test_patch(server): + data = httpx.JSON({"data": 123}) + r = httpx.patch(server.url, content=data) + assert r.code == 200 + assert json.loads(r.body) == { + 'method': 'PATCH', + 'query-params': {}, + 'content-type': 'application/json', + 'json': {"data": 123}, + } + + +def test_delete(server): + r = httpx.delete(server.url) + assert r.code == 200 + assert json.loads(r.body) == { + 'method': 'DELETE', + 'query-params': {}, + 'content-type': None, + 'json': None, + } \ No newline at end of file