Skip to content

Commit 214b235

Browse files
committed
# 0.1.2
* added `auto_read_body` to request and backends in order to automatically read response body so that all retry logic could happen in case of errors
1 parent f3f15e6 commit 214b235

7 files changed

Lines changed: 93 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# 0.1.2
2+
* added `auto_read_body` to request and backends in order to automatically read response body so that all retry logic could happen in case of errors
3+
14
# 0.1.1
25
* Response: added `original` proxy property
36

extapi/http/abc.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ async def get(
4545
data: Any = None,
4646
headers: CIMultiDict | Mapping[str, Any] | None = None,
4747
timeout: Any | float | None = None,
48+
auto_read_body: bool | None = None,
4849
**kwargs,
4950
) -> Response[T_co]:
5051
return await self.execute(
@@ -56,6 +57,7 @@ async def get(
5657
data=data,
5758
headers=_map_headers(headers),
5859
timeout=timeout,
60+
auto_read_body=auto_read_body,
5961
kwargs=kwargs,
6062
)
6163
)
@@ -69,6 +71,7 @@ async def post(
6971
data: Any = None,
7072
headers: CIMultiDict | Mapping[str, Any] | None = None,
7173
timeout: Any | float | None = None,
74+
auto_read_body: bool | None = None,
7275
**kwargs,
7376
) -> Response[T_co]:
7477
return await self.execute(
@@ -80,6 +83,7 @@ async def post(
8083
data=data,
8184
headers=_map_headers(headers),
8285
timeout=timeout,
86+
auto_read_body=auto_read_body,
8387
kwargs=kwargs,
8488
)
8589
)
@@ -93,6 +97,7 @@ async def delete(
9397
data: Any = None,
9498
headers: CIMultiDict | Mapping[str, Any] | None = None,
9599
timeout: Any | float | None = None,
100+
auto_read_body: bool | None = None,
96101
**kwargs,
97102
) -> Response[T_co]:
98103
return await self.execute(
@@ -104,6 +109,7 @@ async def delete(
104109
data=data,
105110
headers=_map_headers(headers),
106111
timeout=timeout,
112+
auto_read_body=auto_read_body,
107113
kwargs=kwargs,
108114
)
109115
)
@@ -117,6 +123,7 @@ async def put(
117123
data: Any = None,
118124
headers: CIMultiDict | Mapping[str, Any] | None = None,
119125
timeout: Any | float | None = None,
126+
auto_read_body: bool | None = None,
120127
**kwargs,
121128
) -> Response[T_co]:
122129
return await self.execute(
@@ -128,6 +135,7 @@ async def put(
128135
data=data,
129136
headers=_map_headers(headers),
130137
timeout=timeout,
138+
auto_read_body=auto_read_body,
131139
kwargs=kwargs,
132140
)
133141
)
@@ -141,6 +149,7 @@ async def patch(
141149
data: Any = None,
142150
headers: CIMultiDict | Mapping[str, Any] | None = None,
143151
timeout: Any | float | None = None,
152+
auto_read_body: bool | None = None,
144153
**kwargs,
145154
) -> Response[T_co]:
146155
return await self.execute(
@@ -152,6 +161,7 @@ async def patch(
152161
data=data,
153162
headers=_map_headers(headers),
154163
timeout=timeout,
164+
auto_read_body=auto_read_body,
155165
kwargs=kwargs,
156166
)
157167
)

extapi/http/backends/aiohttp.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
class AiohttpResponseWrap(BackendResponseProtocol[aiohttp.ClientResponse]):
1616
__slots__ = ("_original",)
1717

18-
def __init__(self, response: aiohttp.ClientResponse):
18+
def __init__(self, response: aiohttp.ClientResponse, *, body: bytes | None = None):
1919
self._original = response
20+
self._body = body
2021

2122
def original(self) -> aiohttp.ClientResponse:
2223
return self._original
@@ -26,6 +27,10 @@ async def close(self) -> None:
2627
await self._original.wait_for_close()
2728

2829
async def read(self) -> bytes:
30+
if self._body is not None:
31+
return self._body
32+
33+
# if body is not supplied - delegate to original
2934
return await self._original.read()
3035

3136
async def json(
@@ -34,6 +39,14 @@ async def json(
3439
encoding: str | None,
3540
loads: Callable[[str], Any] = DEFAULT_JSON_DECODER,
3641
) -> Any:
42+
if self._body is not None:
43+
if encoding is None:
44+
s = self._body.decode()
45+
else:
46+
s = self._body.decode(encoding=encoding)
47+
return loads(s)
48+
49+
# if body is not supplied - delegate to original
3750
return await self._original.json(encoding=encoding, loads=loads)
3851

3952

@@ -67,12 +80,18 @@ class AiohttpExecutor(AbstractExecutor[aiohttp.ClientResponse]):
6780
)
6881

6982
def __init__(
70-
self, *args, ssl: bool | Any = True, default_timeout: float = 10.0, **kwargs
83+
self,
84+
*args,
85+
ssl: bool | Any = True,
86+
default_timeout: float = 10.0,
87+
auto_read_body: bool = True,
88+
**kwargs,
7189
):
7290
super().__init__()
7391
self._ssl = ssl
7492
self._session = self._make_session(*args, **kwargs)
7593
self._default_timeout = default_timeout
94+
self._auto_read_body = auto_read_body
7695

7796
def _make_session(self, *args, **kwargs) -> aiohttp.ClientSession:
7897
return aiohttp.ClientSession(*args, **kwargs)
@@ -82,6 +101,11 @@ async def close(self):
82101

83102
async def execute(self, request: RequestData) -> Response[aiohttp.ClientResponse]:
84103
timeout = request.timeout or self._default_timeout
104+
auto_read_body = (
105+
request.auto_read_body
106+
if request.auto_read_body is not None
107+
else self._auto_read_body
108+
)
85109

86110
# aiohttp-specific kwargs
87111
# we need to pull them individually because
@@ -104,10 +128,14 @@ async def execute(self, request: RequestData) -> Response[aiohttp.ClientResponse
104128
**aiohttp_kwargs,
105129
)
106130

131+
body: bytes | None = None
132+
if auto_read_body:
133+
body = await response.read()
134+
107135
return Response[aiohttp.ClientResponse](
108136
method=request.method,
109137
url=request.url,
110138
status=response.status,
111139
headers=response.headers.copy(),
112-
backend_response=AiohttpResponseWrap(response),
140+
backend_response=AiohttpResponseWrap(response, body=body),
113141
)

extapi/http/backends/httpx.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
class HttpxResponseWrap(BackendResponseProtocol[httpx.Response]):
1515
__slots__ = ("_original",)
1616

17-
def __init__(self, response: httpx.Response):
17+
def __init__(self, response: httpx.Response, *, body: bytes | None = None):
1818
self._original = response
19+
self._body = body
1920

2021
def original(self) -> httpx.Response:
2122
return self._original
@@ -24,6 +25,10 @@ async def close(self) -> None:
2425
return await self._original.aclose()
2526

2627
async def read(self) -> bytes:
28+
if self._body is not None:
29+
return self._body
30+
31+
# if body is not supplied - delegate to original
2732
return await self._original.aread()
2833

2934

@@ -49,6 +54,7 @@ def __init__(
4954
ssl: bool = True,
5055
default_timeout: float = 10.0,
5156
follow_redirects: bool = True,
57+
auto_read_body: bool = True,
5258
**kwargs,
5359
):
5460
super().__init__()
@@ -59,6 +65,7 @@ def __init__(
5965
verify=verify, follow_redirects=follow_redirects, **kwargs
6066
)
6167
self._default_timeout = default_timeout
68+
self._auto_read_body = auto_read_body
6269

6370
def _make_client(self, *args, **kwargs) -> httpx.AsyncClient:
6471
return httpx.AsyncClient(*args, **kwargs)
@@ -68,6 +75,12 @@ async def close(self):
6875

6976
async def execute(self, request: RequestData) -> Response[httpx.Response]:
7077
timeout = request.timeout or self._default_timeout
78+
auto_read_body = (
79+
request.auto_read_body
80+
if request.auto_read_body is not None
81+
else self._auto_read_body
82+
)
83+
7184
url = str(request.url)
7285

7386
if request.headers is None:
@@ -95,10 +108,14 @@ async def execute(self, request: RequestData) -> Response[httpx.Response]:
95108
**httpx_kwargs,
96109
).__aenter__()
97110

111+
body: bytes | None = None
112+
if auto_read_body:
113+
body = await response.aread()
114+
98115
return Response[httpx.Response](
99116
method=request.method,
100117
url=request.url,
101118
status=response.status_code,
102119
headers=CIMultiDict(response.headers),
103-
backend_response=HttpxResponseWrap(response),
120+
backend_response=HttpxResponseWrap(response, body=body),
104121
)

extapi/http/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class RequestData:
2929
data: Any = None
3030
headers: CIMultiDict | None = None
3131
timeout: Any | float | None = None
32+
auto_read_body: bool | None = None
3233
kwargs: dict[str, Any] = field(default_factory=dict)
3334

3435

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "extapi"
3-
version = "0.1.1"
3+
version = "0.1.2"
44
description = "External API library"
55
authors = [
66
{ name = "KTS", email = "hello@kts.tech" }

tests/exthttp/backends/test_aiohttp.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,18 +44,46 @@ async def test_read(self, dummy_server: TestServer):
4444
request = RequestData(
4545
method="GET",
4646
url=URL(f"http://localhost:{dummy_server.port}/get"),
47+
auto_read_body=False,
4748
)
4849

4950
response = await executor.execute(request)
5051
async with response:
5152
res = await response.read()
5253
assert res == b'{"status": "ok"}'
5354

55+
async def test_read_supplied(self, dummy_server: TestServer):
56+
async with AiohttpExecutor() as executor:
57+
request = RequestData(
58+
method="GET",
59+
url=URL(f"http://localhost:{dummy_server.port}/get"),
60+
auto_read_body=True,
61+
)
62+
63+
response = await executor.execute(request)
64+
async with response:
65+
res = await response.read()
66+
assert res == b'{"status": "ok"}'
67+
68+
async def test_json_supplied(self, dummy_server: TestServer):
69+
async with AiohttpExecutor() as executor:
70+
request = RequestData(
71+
method="GET",
72+
url=URL(f"http://localhost:{dummy_server.port}/get"),
73+
auto_read_body=True,
74+
)
75+
76+
response = await executor.execute(request)
77+
async with response:
78+
res = await response.json()
79+
assert res == {"status": "ok"}
80+
5481
async def test_json(self, dummy_server: TestServer):
5582
async with AiohttpExecutor() as executor:
5683
request = RequestData(
5784
method="GET",
5885
url=URL(f"http://localhost:{dummy_server.port}/get"),
86+
auto_read_body=False,
5987
)
6088

6189
response = await executor.execute(request)

0 commit comments

Comments
 (0)