Skip to content

Commit

Permalink
Merge SyncResponse and AsyncResponse into single Response class. Repl…
Browse files Browse the repository at this point in the history
…ace .read() with .content
  • Loading branch information
dvolodin7 committed Mar 14, 2024
1 parent fe5f8ff commit 57e9242
Show file tree
Hide file tree
Showing 27 changed files with 267 additions and 324 deletions.
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ The getting of single URL is a simple task:
async with HttpClient() as client:
resp = client.get("https://docs.gufolabs.com/")
assert resp.status == 200
data = await resp.read()
data = resp.content
```

The `HttpClient` is highly customizable, for example,
Expand All @@ -40,6 +40,16 @@ async with HttpClient(headers={"X-My-Header": b"test"}) as client:
...
```

The response headers processing as easy as working with dicts:

``` python
async with HttpClient(headers={"X-My-Header": b"test"}) as client:
resp = client.get("https://docs.gufolabs.com/")
if resp.headers["Content-Type"] == "text/html":
...
```


Gufo HTTP supports common authentication methods out-of-box:

``` python
Expand All @@ -48,6 +58,18 @@ async with HttpClient(auth=BasicAuth("scott", "tiger")) as client:
...
```

## Features

* Clean async and blocking API.
* High performance (see [Performance](#performance) section for details).
* Built with security in mind.
* Customizabile redirect policy.
* TLS support.
* Basic and bearer authorization schemes.
* Full Python typing support.
* Editor completion.
* Well-tested, battle-proven code.

## Performance

Gufo HTTP is proved to be one of the fastest Python HTTP client available
Expand Down
4 changes: 2 additions & 2 deletions benchmarks/test_https_linear_x100_1k.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def bench():
with SyncHttpClient(validate_cert=False) as client:
for _ in range(REPEATS):
resp = client.get(url)
resp.read()
_ = resp.content


def test_gufo_http_async(httpd: Httpd, benchmark) -> None:
Expand All @@ -67,7 +67,7 @@ async def inner():
async with AsyncHttpClient(validate_cert=False) as client:
for _ in range(REPEATS):
resp = await client.get(url)
await resp.read()
_ = resp.content

asyncio.run(inner())

Expand Down
4 changes: 2 additions & 2 deletions benchmarks/test_https_p4_x100_1k.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def do_request():
with SyncHttpClient(validate_cert=False) as client:
for _ in range(PER_TASK):
resp = client.get(url)
resp.read()
_ = resp.content

@benchmark
def bench():
Expand All @@ -84,7 +84,7 @@ async def do_request():
async with AsyncHttpClient(validate_cert=False) as client:
for _ in range(PER_TASK):
resp = await client.get(url)
await resp.read()
_ = resp.content

@benchmark
def bench():
Expand Down
4 changes: 2 additions & 2 deletions benchmarks/test_https_single_x100_1k.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def test_gufo_http_sync(httpd: Httpd, benchmark) -> None:
def bench():
with SyncHttpClient(validate_cert=False) as client:
resp = client.get(url)
resp.read()
_ = resp.content


def test_gufo_http_async(httpd: Httpd, benchmark) -> None:
Expand All @@ -64,7 +64,7 @@ def bench():
async def inner():
async with AsyncHttpClient(validate_cert=False) as client:
resp = await client.get(url)
await resp.read()
_ = resp.content

asyncio.run(inner())

Expand Down
4 changes: 2 additions & 2 deletions benchmarks/test_linear_x100_1k.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def bench():
with SyncHttpClient() as client:
for _ in range(REPEATS):
resp = client.get(url)
resp.read()
_ = resp.content


def test_gufo_http_async(httpd: Httpd, benchmark) -> None:
Expand All @@ -65,7 +65,7 @@ async def inner():
async with AsyncHttpClient() as client:
for _ in range(REPEATS):
resp = await client.get(url)
await resp.read()
_ = resp.content

asyncio.run(inner())

Expand Down
4 changes: 2 additions & 2 deletions benchmarks/test_p4_x100_1k.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def do_request():
with SyncHttpClient() as client:
for _ in range(PER_TASK):
resp = client.get(url)
resp.read()
_ = resp.content

@benchmark
def bench():
Expand All @@ -82,7 +82,7 @@ async def do_request():
async with AsyncHttpClient() as client:
for _ in range(PER_TASK):
resp = await client.get(url)
await resp.read()
_ = resp.content

@benchmark
def bench():
Expand Down
4 changes: 2 additions & 2 deletions benchmarks/test_single_x100_1k.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def test_gufo_http_sync(httpd: Httpd, benchmark) -> None:
def bench():
with SyncHttpClient() as client:
resp = client.get(url)
resp.read()
_ = resp.content


def test_gufo_http_async(httpd: Httpd, benchmark) -> None:
Expand All @@ -62,7 +62,7 @@ def bench():
async def inner():
async with AsyncHttpClient() as client:
resp = await client.get(url)
await resp.read()
_ = resp.content

asyncio.run(inner())

Expand Down
13 changes: 4 additions & 9 deletions docs/examples/async/get.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,18 +75,13 @@ If status code is not OK, we print and error message and terminate our function.
``` py title="get.py" linenums="1" hl_lines="13"
--8<-- "examples/async/get.py"
```
`.read()` method waits and returns a whole response body.
This is an asyncronous function which needs to be awaited.


``` py title="get.py" linenums="1" hl_lines="14"
--8<-- "examples/async/get.py"
```
Here we print our response body. Note, `.read()` returns a `bytes` type,
The `.content` attribute of `Response` contains the response
body. This attribute has type of `bytes`,
so we convert it into `str` using `.decode()` method. In our example
we consider the response is encoding using UTF-8 encoding.
Then we print decoded result.

``` py title="get.py" linenums="1" hl_lines="17"
``` py title="get.py" linenums="1" hl_lines="16"
--8<-- "examples/async/get.py"
```
Lets run our `main()` function pass first command-line parameter as url.
Expand Down
10 changes: 4 additions & 6 deletions docs/examples/sync/get.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,13 @@ If status code is not OK, we print and error message and terminate our function.
``` py title="get.py" linenums="1" hl_lines="12"
--8<-- "examples/sync/get.py"
```
`.read()` method waits and returns a whole response body.
``` py title="get.py" linenums="1" hl_lines="13"
--8<-- "examples/sync/get.py"
```
Here we print our response body. Note, `.read()` returns a `bytes` type,
The `.content` attribute of `Response` contains the response
body. This attribute has type of `bytes`,
so we convert it into `str` using `.decode()` method. In our example
we consider the response is encoding using UTF-8 encoding.
Then we print decoded result.

``` py title="get.py" linenums="1" hl_lines="16"
``` py title="get.py" linenums="1" hl_lines="15"
--8<-- "examples/sync/get.py"
```
Lets run our `main()` function pass first command-line parameter as url.
Expand Down
23 changes: 22 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ The getting of single URL is a simple task:
async with HttpClient() as client:
resp = client.get("https://docs.gufolabs.com/")
assert resp.status == 200
data = await resp.read()
data = resp.content
```

The `HttpClient` is highly customizable, for example,
Expand All @@ -41,6 +41,27 @@ async with HttpClient(auth=BasicAuth("scott", "tiger")) as client:
...
```

The response headers processing as easy as working with dicts:

``` python
async with HttpClient(headers={"X-My-Header": b"test"}) as client:
resp = client.get("https://docs.gufolabs.com/")
if resp.headers["Content-Type"] == "text/html":
...
```

## Features

* Clean async and blocking API.
* High performance (see [Performance](#performance) section for details).
* Built with security in mind.
* Customizabile redirect policy.
* TLS support.
* Basic and bearer authorization schemes.
* Full Python typing support.
* Editor completion.
* Well-tested, battle-proven code.

## Performance

Gufo HTTP is proved to be one of the fastest Python HTTP client available
Expand Down
3 changes: 1 addition & 2 deletions examples/async/get.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ async def main(url: str) -> None:
if r.status != 200:
print(f"Invalid response code: {r.status}")
return
data = await r.read()
print(data.decode())
print(r.content.decode())


asyncio.run(main(sys.argv[1]))
3 changes: 1 addition & 2 deletions examples/sync/get.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ def main(url: str) -> None:
if r.status != 200:
print(f"Invalid response code: {r.status}")
return
data = r.read()
print(data.decode())
print(r.content.decode())


main(sys.argv[1])
69 changes: 45 additions & 24 deletions src/async_client/client.rs → src/async_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
// Copyright (C) 2024, Gufo Labs
// See LICENSE.md for details
// ------------------------------------------------------------------------
use super::response::AsyncResponse;
use crate::auth::{AuthMethod, BasicAuth, BearerAuth, GetAuthMethod};
use crate::error::HttpError;
use crate::headers::Headers;
use crate::method::{get_method, BROTLI, DEFLATE, GZIP};
use crate::response::Response;
use pyo3::{
exceptions::{PyTypeError, PyValueError},
prelude::*,
types::PyBytes,
};
use pyo3_asyncio::tokio::future_into_py;
use reqwest::{
Expand Down Expand Up @@ -113,33 +115,52 @@ impl AsyncClient {
headers: Option<HashMap<&str, &[u8]>>,
body: Option<Vec<u8>>,
) -> PyResult<&'a PyAny> {
// Build request for method
let mut req = self.client.request(get_method(method)?, url);
// Add headers
if let Some(h) = headers {
for (k, v) in h {
req = req.header(
HeaderName::from_bytes(k.as_ref())
.map_err(|e| PyValueError::new_err(e.to_string()))?,
HeaderValue::from_bytes(v).map_err(|e| PyValueError::new_err(e.to_string()))?,
)
// Get method
let method = get_method(method)?;
let req = py.allow_threads(|| -> Result<reqwest::RequestBuilder, HttpError> {
// Build request for method
let mut req = self.client.request(method, url);
// Add headers
if let Some(h) = headers {
for (k, v) in h {
req = req.header(
HeaderName::from_bytes(k.as_ref())
.map_err(|e| HttpError::ValueError(e.to_string()))?,
HeaderValue::from_bytes(v)
.map_err(|e| HttpError::ValueError(e.to_string()))?,
)
}
}
}
// Add auth
match &self.auth {
AuthMethod::None => {}
AuthMethod::Basic { user, password } => req = req.basic_auth(user, password.as_ref()),
AuthMethod::Bearer { token } => req = req.bearer_auth(token),
}
// Add body
if let Some(b) = body {
req = req.body(b);
}
// Add auth
match &self.auth {
AuthMethod::None => {}
AuthMethod::Basic { user, password } => {
req = req.basic_auth(user, password.as_ref())
}
AuthMethod::Bearer { token } => req = req.bearer_auth(token),
}
// Add body
if let Some(b) = body {
req = req.body(b);
}
Ok(req)
})?;
// Create future
future_into_py(py, async move {
// Send request and wait for response
let response = req.send().await.map_err(HttpError::from)?;
Ok(AsyncResponse::new(response))
let resp = req.send().await.map_err(HttpError::from)?;
// Get status
let status: u16 = resp.status().into();
// Wrap headers
let headers = Headers::new(resp.headers().clone());
// Read body
let buf = resp.bytes().await.map_err(HttpError::from)?;
// Return response
Ok(Response::new(
status,
headers,
Python::with_gil(|py| PyBytes::new(py, buf.as_ref()).into()),
))
})
}
}
10 changes: 0 additions & 10 deletions src/async_client/mod.rs

This file was deleted.

Loading

0 comments on commit 57e9242

Please sign in to comment.