Skip to content

Commit 9a11bb4

Browse files
committed
Response now has async read() method instead of data property + reorganize Response's typings
1 parent 9fd2e8d commit 9a11bb4

22 files changed

Lines changed: 383 additions & 286 deletions

extapi/http/abc.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55

66
from .types import RequestData, Response, StrOrURL
77

8+
T_co = TypeVar("T_co", covariant=True)
9+
T_contr = TypeVar("T_contr", contravariant=True)
810
T = TypeVar("T")
911

1012

11-
class AbstractExecutor(Generic[T], metaclass=abc.ABCMeta):
13+
class AbstractExecutor(Generic[T_co], metaclass=abc.ABCMeta):
1214
async def start(self) -> None:
1315
return None
1416

@@ -22,14 +24,14 @@ async def __aenter__(self) -> Self:
2224
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
2325
await self.close()
2426

25-
def generalize(self) -> "AbstractExecutor[T]":
27+
def generalize(self) -> "AbstractExecutor[T_co]":
2628
return self
2729

2830
@abc.abstractmethod
2931
async def execute(
3032
self,
3133
request: RequestData,
32-
) -> Response[T]:
34+
) -> Response[T_co]:
3335
raise NotImplementedError # pragma: no cover
3436

3537
async def get(
@@ -42,7 +44,7 @@ async def get(
4244
headers: CIMultiDict | None = None,
4345
timeout: Any | float | None = None,
4446
**kwargs,
45-
) -> Response[T]:
47+
) -> Response[T_co]:
4648
return await self.execute(
4749
RequestData(
4850
method="GET",
@@ -66,7 +68,7 @@ async def post(
6668
headers: CIMultiDict | None = None,
6769
timeout: Any | float | None = None,
6870
**kwargs,
69-
) -> Response[T]:
71+
) -> Response[T_co]:
7072
return await self.execute(
7173
RequestData(
7274
method="POST",
@@ -90,7 +92,7 @@ async def delete(
9092
headers: CIMultiDict | None = None,
9193
timeout: Any | float | None = None,
9294
**kwargs,
93-
) -> Response[T]:
95+
) -> Response[T_co]:
9496
return await self.execute(
9597
RequestData(
9698
method="DELETE",
@@ -114,7 +116,7 @@ async def put(
114116
headers: CIMultiDict | None = None,
115117
timeout: Any | float | None = None,
116118
**kwargs,
117-
) -> Response[T]:
119+
) -> Response[T_co]:
118120
return await self.execute(
119121
RequestData(
120122
method="PUT",
@@ -138,7 +140,7 @@ async def patch(
138140
headers: CIMultiDict | None = None,
139141
timeout: Any | float | None = None,
140142
**kwargs,
141-
) -> Response[T]:
143+
) -> Response[T_co]:
142144
return await self.execute(
143145
RequestData(
144146
method="PATCH",
@@ -154,8 +156,10 @@ async def patch(
154156

155157

156158
@runtime_checkable
157-
class Retryable(Protocol[T]):
158-
async def need_retry(self, response: Response[T]) -> tuple[bool, float | None]: ...
159+
class Retryable(Protocol[T_contr]):
160+
async def need_retry(
161+
self, response: Response[T_contr]
162+
) -> tuple[bool, float | None]: ...
159163

160164

161165
@runtime_checkable

extapi/http/addons/log.py

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,41 +2,57 @@
22
import logging
33
from typing import Generic, TypeVar
44

5+
from yarl import URL
6+
57
from extapi.http.abc import Addon
68
from extapi.http.types import HttpExecuteError, RequestData, Response
79

810
T = TypeVar("T")
911

1012

1113
class LoggingAddon(Addon[T], Generic[T]):
12-
def __init__(self):
14+
def __init__(self, *, log_params: bool = True):
1315
self._logger = logging.getLogger("extapi.http.addons.log")
16+
self._log_params = log_params
17+
18+
def _get_url(self, request: RequestData) -> URL:
19+
url_ = URL(request.url) if not isinstance(request.url, URL) else request.url
20+
21+
if self._log_params and request.params:
22+
return url_.with_query(request.params)
23+
24+
return url_
1425

1526
async def before_request(self, request: RequestData) -> None:
16-
self._logger.debug("executing request %s %s", request.method, str(request.url))
27+
url = self._get_url(request)
28+
self._logger.debug("executing request %s %s", request.method, str(url))
1729

1830
async def process_response(
1931
self, request: RequestData, response: Response[T]
2032
) -> Response[T]:
33+
url = self._get_url(request)
34+
2135
logger_method = (
2236
self._logger.debug if response.status < 500 else self._logger.error
2337
)
2438

2539
logger_method(
2640
"received response %s %s -> status=%s",
2741
request.method,
28-
str(request.url),
42+
str(url),
2943
response.status if response is not None else "unknown",
3044
)
3145

3246
return response
3347

3448
async def process_error(self, request: RequestData, error: Exception) -> None:
49+
url = self._get_url(request)
50+
3551
if isinstance(error, TimeoutError):
3652
self._logger.error(
3753
"timeout error for request %s %s failed with error %s(%s)",
3854
request.method,
39-
str(request.url),
55+
str(url),
4056
type(error).__name__,
4157
str(error),
4258
)
@@ -46,7 +62,7 @@ async def process_error(self, request: RequestData, error: Exception) -> None:
4662
self._logger.error(
4763
"request %s %s failed with error %s(%s)",
4864
request.method,
49-
str(request.url),
65+
str(url),
5066
type(error).__name__,
5167
error,
5268
)
@@ -56,12 +72,17 @@ class VerboseLoggingExecutor(LoggingAddon[T], Generic[T]):
5672
def __init__(
5773
self,
5874
*,
75+
log_params: bool = True,
76+
log_response_data: bool = True,
5977
truncate_response_data: int | None = 1024,
6078
):
61-
super().__init__()
79+
super().__init__(log_params=log_params)
80+
self._log_response_data = log_response_data
6281
self._truncate_response_data = truncate_response_data
6382

6483
async def before_request(self, request: RequestData) -> None:
84+
url = self._get_url(request)
85+
6586
json = request.json
6687
if json is not None:
6788
if isinstance(json, bytes):
@@ -71,7 +92,7 @@ async def before_request(self, request: RequestData) -> None:
7192
self._logger.debug(
7293
"executing request %s %s with params=%s json=%s data=%s timeout=%s",
7394
request.method,
74-
str(request.url),
95+
str(url),
7596
request.params,
7697
json,
7798
request.data,
@@ -81,18 +102,22 @@ async def before_request(self, request: RequestData) -> None:
81102
async def process_response(
82103
self, request: RequestData, response: Response[T]
83104
) -> Response[T]:
105+
url = self._get_url(request)
106+
84107
logger_method = (
85108
self._logger.debug if response.status < 500 else self._logger.error
86109
)
87-
88-
resp_body = response.data.decode("utf-8") if response.has_data else None
89-
if resp_body is not None and self._truncate_response_data is not None:
90-
resp_body = resp_body[: self._truncate_response_data]
110+
resp_body: str | None = None
111+
if self._log_response_data:
112+
resp_body_bytes = await response.read()
113+
if self._truncate_response_data is not None:
114+
resp_body_bytes = resp_body_bytes[: self._truncate_response_data]
115+
resp_body = resp_body_bytes.decode("utf-8")
91116

92117
logger_method(
93118
"received response %s %s -> status=%s headers=%s body=%s",
94119
request.method,
95-
str(request.url),
120+
str(url),
96121
response.status,
97122
response.headers,
98123
resp_body,

extapi/http/addons/retry.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,35 @@
77

88

99
class Retry5xxAddon(Retryable[T], Generic[T]):
10-
__slots__ = ("_retry_timeout",)
10+
__slots__ = ("_default_timeout",)
1111

12-
def __init__(self, *, retry_timeout: float | None = None):
13-
self._retry_timeout = retry_timeout
12+
def __init__(self, *, default_timeout: float | None = None):
13+
self._default_timeout = default_timeout
1414

1515
async def need_retry(self, response: Response[T]) -> tuple[bool, float | None]:
1616
if response.status >= 500:
17-
return True, self._retry_timeout
17+
return True, self._default_timeout
1818

1919
return False, None
2020

2121

2222
class Retry429Addon(Retryable[T], Generic[T]):
23+
__slots__ = ("_default_timeout",)
24+
25+
def __init__(self, *, default_timeout: float | None = None):
26+
self._default_timeout = default_timeout
27+
2328
async def need_retry(self, response: Response[T]) -> tuple[bool, float | None]:
2429
if response.status != 429:
2530
return False, None
2631

2732
retry_after_s = response.headers.get("retry-after")
2833
if retry_after_s is not None:
34+
retry_after: float | None
2935
try:
3036
retry_after = float(retry_after_s)
3137
except ValueError:
32-
retry_after = None
38+
retry_after = self._default_timeout
3339

3440
return True, retry_after
3541

extapi/http/backends/aiohttp.py

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,27 @@
33
import aiohttp
44

55
from extapi.http.abc import AbstractExecutor
6-
from extapi.http.types import Closable, RequestData, Response
6+
from extapi.http.types import BackendResponseProtocol, RequestData, Response
77

88

9-
class AiohttpResponseWrap(Closable):
10-
__slots__ = ("original",)
9+
class AiohttpResponseWrap(BackendResponseProtocol[aiohttp.ClientResponse]):
10+
__slots__ = ("_original",)
1111

1212
def __init__(self, response: aiohttp.ClientResponse):
13-
self.original = response
13+
self._original = response
14+
15+
def original(self) -> aiohttp.ClientResponse:
16+
return self._original
1417

1518
async def close(self) -> None:
16-
self.original.release()
17-
await self.original.wait_for_close()
19+
self._original.release()
20+
await self._original.wait_for_close()
21+
22+
async def read(self) -> bytes:
23+
return await self._original.read()
1824

1925

20-
class AiohttpStreamingExecutor(AbstractExecutor[AiohttpResponseWrap]):
26+
class AiohttpExecutor(AbstractExecutor[aiohttp.ClientResponse]):
2127
__slots__ = (
2228
"_ssl",
2329
"_session",
@@ -35,7 +41,7 @@ def __init__(
3541
async def close(self):
3642
await self._session.close()
3743

38-
async def execute(self, request: RequestData) -> Response[AiohttpResponseWrap]:
44+
async def execute(self, request: RequestData) -> Response[aiohttp.ClientResponse]:
3945
timeout = request.timeout or self._default_timeout
4046

4147
response = await self._session.request(
@@ -50,17 +56,9 @@ async def execute(self, request: RequestData) -> Response[AiohttpResponseWrap]:
5056
**request.kwargs,
5157
)
5258

53-
return Response[AiohttpResponseWrap](
59+
return Response[aiohttp.ClientResponse](
5460
url=request.url,
5561
status=response.status,
5662
headers=response.headers.copy(),
5763
backend_response=AiohttpResponseWrap(response),
5864
)
59-
60-
61-
class AiohttpExecutor(AiohttpStreamingExecutor):
62-
async def execute(self, request: RequestData) -> Response[AiohttpResponseWrap]:
63-
resp = await super().execute(request)
64-
async with resp:
65-
resp.set_data(await resp.backend_response.original.read())
66-
return resp

0 commit comments

Comments
 (0)