Skip to content

Commit d5cff9d

Browse files
committed
reorganize page_inputs.py as a submodule; move HttpClient to it
1 parent 753e6ad commit d5cff9d

File tree

7 files changed

+179
-174
lines changed

7 files changed

+179
-174
lines changed

tests/test_requests.py

+3-5
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,13 @@
33
import pytest
44
from web_poet.exceptions import RequestBackendError
55
from web_poet.page_inputs import (
6+
HttpClient,
67
HttpRequest,
78
HttpResponse,
89
HttpRequestBody,
910
HttpRequestHeaders
1011
)
11-
from web_poet.requests import (
12-
HttpClient,
13-
request_backend_var,
14-
)
12+
from web_poet.requests import request_backend_var
1513

1614

1715
@pytest.fixture
@@ -47,7 +45,7 @@ async def test_perform_request_from_httpclient(async_mock):
4745
async def test_http_client_single_requests(async_mock):
4846
client = HttpClient(async_mock)
4947

50-
with mock.patch("web_poet.requests.HttpRequest") as mock_request:
48+
with mock.patch("web_poet.page_inputs.client.HttpRequest") as mock_request:
5149
response = await client.request("url")
5250
response.url == "url"
5351

web_poet/__init__.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
from .pages import WebPage, ItemPage, ItemWebPage, Injectable
2-
from .requests import (
3-
request_backend_var,
4-
HttpClient,
5-
)
2+
from .requests import request_backend_var
63
from .page_inputs import (
74
Meta,
5+
HttpClient,
86
HttpRequest,
97
HttpResponse,
108
HttpRequestHeaders,

web_poet/page_inputs/__init__.py

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from .meta import Meta
2+
from .client import HttpClient
3+
from .http import (
4+
HttpRequest,
5+
HttpResponse,
6+
HttpRequestHeaders,
7+
HttpResponseHeaders,
8+
HttpRequestBody,
9+
HttpResponseBody,
10+
)

web_poet/page_inputs/client.py

+153
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
"""This module has a full support for :mod:`asyncio` that enables developers to
2+
perform asynchronous additional requests inside of Page Objects.
3+
4+
Note that the implementation to fully execute any :class:`~.Request` is not
5+
handled in this module. With that, the framework using **web-poet** must supply
6+
the implementation.
7+
8+
You can read more about this in the :ref:`advanced-downloader-impl` documentation.
9+
"""
10+
11+
import asyncio
12+
import logging
13+
from typing import Optional, Dict, List, Union, Callable
14+
15+
from web_poet.requests import request_backend_var
16+
from web_poet.exceptions import RequestBackendError
17+
from web_poet.page_inputs.http import (
18+
HttpRequest,
19+
HttpRequestHeaders,
20+
HttpRequestBody,
21+
HttpResponse,
22+
)
23+
24+
logger = logging.getLogger(__name__)
25+
26+
_StrMapping = Dict[str, str]
27+
_Headers = Union[_StrMapping, HttpRequestHeaders]
28+
_Body = Union[bytes, HttpRequestBody]
29+
30+
31+
async def _perform_request(request: HttpRequest) -> HttpResponse:
32+
"""Given a :class:`~.Request`, execute it using the **request implementation**
33+
that was set in the ``web_poet.request_backend_var`` :mod:`contextvars`
34+
instance.
35+
36+
.. warning::
37+
By convention, this function should return a :class:`~.HttpResponse`.
38+
However, the underlying downloader assigned in
39+
``web_poet.request_backend_var`` might change that, depending on
40+
how the framework using **web-poet** implements it.
41+
"""
42+
43+
logger.info(f"Requesting page: {request}")
44+
45+
try:
46+
request_backend = request_backend_var.get()
47+
except LookupError:
48+
raise RequestBackendError(
49+
"Additional requests are used inside the Page Object but the "
50+
"current framework has not set any HttpRequest Backend via "
51+
"'web_poet.request_backend_var'"
52+
)
53+
54+
response_data: HttpResponse = await request_backend(request)
55+
return response_data
56+
57+
58+
class HttpClient:
59+
"""A convenient client to easily execute requests.
60+
61+
By default, it uses the request implementation assigned in the
62+
``web_poet.request_backend_var`` which is a :mod:`contextvars` instance to
63+
download the actual requests. However, it can easily be overridable by
64+
providing an optional ``request_downloader`` callable.
65+
66+
Providing the request implementation by dependency injection would be a good
67+
alternative solution when you want to avoid setting up :mod:`contextvars`
68+
like ``web_poet.request_backend_var``.
69+
70+
In any case, this doesn't contain any implementation about how to execute
71+
any requests fed into it. When setting that up, make sure that the downloader
72+
implementation returns a :class:`~.HttpResponse` instance.
73+
"""
74+
75+
def __init__(self, request_downloader: Callable = None):
76+
self._request_downloader = request_downloader or _perform_request
77+
78+
async def request(
79+
self,
80+
url: str,
81+
*,
82+
method: str = "GET",
83+
headers: Optional[_Headers] = None,
84+
body: Optional[_Body] = None,
85+
) -> HttpResponse:
86+
"""This is a shortcut for creating a :class:`~.HttpRequest` instance and executing
87+
that request.
88+
89+
A :class:`~.HttpResponse` instance should then be returned.
90+
91+
.. warning::
92+
By convention, the request implementation supplied optionally to
93+
:class:`~.HttpClient` should return a :class:`~.HttpResponse` instance.
94+
However, the underlying implementation supplied might change that,
95+
depending on how the framework using **web-poet** implements it.
96+
"""
97+
headers = headers or {}
98+
body = body or b""
99+
req = HttpRequest(url=url, method=method, headers=headers, body=body)
100+
return await self.execute(req)
101+
102+
async def get(
103+
self, url: str, *, headers: Optional[_Headers] = None
104+
) -> HttpResponse:
105+
"""Similar to :meth:`~.HttpClient.request` but peforming a ``GET``
106+
request.
107+
"""
108+
return await self.request(url=url, method="GET", headers=headers)
109+
110+
async def post(
111+
self,
112+
url: str,
113+
*,
114+
headers: Optional[_Headers] = None,
115+
body: Optional[_Body] = None,
116+
) -> HttpResponse:
117+
"""Similar to :meth:`~.HttpClient.request` but performing a ``POST``
118+
request.
119+
"""
120+
return await self.request(url=url, method="POST", headers=headers, body=body)
121+
122+
async def execute(self, request: HttpRequest) -> HttpResponse:
123+
"""Accepts a single instance of :class:`~.HttpRequest` and executes it
124+
using the request implementation configured in the :class:`~.HttpClient`
125+
instance.
126+
127+
This returns a single :class:`~.HttpResponse`.
128+
"""
129+
return await self._request_downloader(request)
130+
131+
async def batch_execute(
132+
self, *requests: HttpRequest, return_exceptions: bool = False
133+
) -> List[Union[HttpResponse, Exception]]:
134+
"""Similar to :meth:`~.HttpClient.execute` but accepts a collection of
135+
:class:`~.HttpRequest` instances that would be batch executed.
136+
137+
The order of the :class:`~.HttpResponses` would correspond to the order
138+
of :class:`~.HttpRequest` passed.
139+
140+
If any of the :class:`~.HttpRequest` raises an exception upon execution,
141+
the exception is raised.
142+
143+
To prevent this, the actual exception can be returned alongside any
144+
successful :class:`~.HttpResponse`. This enables salvaging any usable
145+
responses despite any possible failures. This can be done by setting
146+
``True`` to the ``return_exceptions`` parameter.
147+
"""
148+
149+
coroutines = [self._request_downloader(r) for r in requests]
150+
responses = await asyncio.gather(
151+
*coroutines, return_exceptions=return_exceptions
152+
)
153+
return responses

web_poet/page_inputs.py renamed to web_poet/page_inputs/http.py

+3-12
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
from web_poet.utils import memoizemethod_noargs
1515

1616
T_headers = TypeVar("T_headers", bound="HttpResponseHeaders")
17-
AnyStrDict = Dict[AnyStr, Union[AnyStr, List[AnyStr], Tuple[AnyStr, ...]]]
17+
18+
_AnyStrDict = Dict[AnyStr, Union[AnyStr, List[AnyStr], Tuple[AnyStr, ...]]]
1819

1920

2021
class HttpRequestBody(bytes):
@@ -99,7 +100,7 @@ class HttpResponseHeaders(_HttpHeaders):
99100

100101
@classmethod
101102
def from_bytes_dict(
102-
cls: Type[T_headers], arg: AnyStrDict, encoding: str = "utf-8"
103+
cls: Type[T_headers], arg: _AnyStrDict, encoding: str = "utf-8"
103104
) -> T_headers:
104105
"""An alternative constructor for instantiation where the header-value
105106
pairs could be in raw bytes form.
@@ -270,13 +271,3 @@ def _auto_detect_fun(self, body: bytes) -> Optional[str]:
270271
except UnicodeError:
271272
continue
272273
return resolve_encoding(enc)
273-
274-
275-
class Meta(dict):
276-
"""Container class that could contain any arbitrary data to be passed into
277-
a Page Object.
278-
279-
Note that this is simply a subclass of Python's ``dict``.
280-
"""
281-
282-
pass

web_poet/page_inputs/meta.py

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
class Meta(dict):
2+
"""Container class that could contain any arbitrary data to be passed into
3+
a Page Object.
4+
5+
Note that this is simply a subclass of Python's ``dict``.
6+
"""
7+
8+
pass

0 commit comments

Comments
 (0)