-
Notifications
You must be signed in to change notification settings - Fork 18
reorganize page_inputs.py as a submodule; move HttpClient to it #37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| from .meta import Meta | ||
| from .client import HttpClient | ||
| from .http import ( | ||
| HttpRequest, | ||
| HttpResponse, | ||
| HttpRequestHeaders, | ||
| HttpResponseHeaders, | ||
| HttpRequestBody, | ||
| HttpResponseBody, | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| """This module has a full support for :mod:`asyncio` that enables developers to | ||
| perform asynchronous additional requests inside of Page Objects. | ||
|
|
||
| Note that the implementation to fully execute any :class:`~.Request` is not | ||
| handled in this module. With that, the framework using **web-poet** must supply | ||
| the implementation. | ||
|
|
||
| You can read more about this in the :ref:`advanced-downloader-impl` documentation. | ||
| """ | ||
|
|
||
| import asyncio | ||
| import logging | ||
| from typing import Optional, Dict, List, Union, Callable | ||
|
|
||
| from web_poet.requests import request_backend_var | ||
| from web_poet.exceptions import RequestBackendError | ||
| from web_poet.page_inputs.http import ( | ||
| HttpRequest, | ||
| HttpRequestHeaders, | ||
| HttpRequestBody, | ||
| HttpResponse, | ||
| ) | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| _StrMapping = Dict[str, str] | ||
| _Headers = Union[_StrMapping, HttpRequestHeaders] | ||
| _Body = Union[bytes, HttpRequestBody] | ||
|
|
||
|
|
||
| async def _perform_request(request: HttpRequest) -> HttpResponse: | ||
| """Given a :class:`~.Request`, execute it using the **request implementation** | ||
| that was set in the ``web_poet.request_backend_var`` :mod:`contextvars` | ||
| instance. | ||
|
|
||
| .. warning:: | ||
| By convention, this function should return a :class:`~.HttpResponse`. | ||
| However, the underlying downloader assigned in | ||
| ``web_poet.request_backend_var`` might change that, depending on | ||
| how the framework using **web-poet** implements it. | ||
| """ | ||
|
|
||
| logger.info(f"Requesting page: {request}") | ||
|
|
||
| try: | ||
| request_backend = request_backend_var.get() | ||
| except LookupError: | ||
| raise RequestBackendError( | ||
| "Additional requests are used inside the Page Object but the " | ||
| "current framework has not set any HttpRequest Backend via " | ||
| "'web_poet.request_backend_var'" | ||
| ) | ||
|
|
||
| response_data: HttpResponse = await request_backend(request) | ||
| return response_data | ||
|
|
||
|
|
||
| class HttpClient: | ||
| """A convenient client to easily execute requests. | ||
|
|
||
| By default, it uses the request implementation assigned in the | ||
| ``web_poet.request_backend_var`` which is a :mod:`contextvars` instance to | ||
| download the actual requests. However, it can easily be overridable by | ||
| providing an optional ``request_downloader`` callable. | ||
|
|
||
| Providing the request implementation by dependency injection would be a good | ||
| alternative solution when you want to avoid setting up :mod:`contextvars` | ||
| like ``web_poet.request_backend_var``. | ||
|
|
||
| In any case, this doesn't contain any implementation about how to execute | ||
| any requests fed into it. When setting that up, make sure that the downloader | ||
| implementation returns a :class:`~.HttpResponse` instance. | ||
| """ | ||
|
|
||
| def __init__(self, request_downloader: Callable = None): | ||
| self._request_downloader = request_downloader or _perform_request | ||
|
|
||
| async def request( | ||
| self, | ||
| url: str, | ||
| *, | ||
| method: str = "GET", | ||
| headers: Optional[_Headers] = None, | ||
| body: Optional[_Body] = None, | ||
| ) -> HttpResponse: | ||
| """This is a shortcut for creating a :class:`~.HttpRequest` instance and executing | ||
| that request. | ||
|
|
||
| A :class:`~.HttpResponse` instance should then be returned. | ||
|
|
||
| .. warning:: | ||
| By convention, the request implementation supplied optionally to | ||
| :class:`~.HttpClient` should return a :class:`~.HttpResponse` instance. | ||
| However, the underlying implementation supplied might change that, | ||
| depending on how the framework using **web-poet** implements it. | ||
| """ | ||
| headers = headers or {} | ||
| body = body or b"" | ||
| req = HttpRequest(url=url, method=method, headers=headers, body=body) | ||
| return await self.execute(req) | ||
|
|
||
| async def get( | ||
| self, url: str, *, headers: Optional[_Headers] = None | ||
| ) -> HttpResponse: | ||
| """Similar to :meth:`~.HttpClient.request` but peforming a ``GET`` | ||
| request. | ||
| """ | ||
| return await self.request(url=url, method="GET", headers=headers) | ||
|
|
||
| async def post( | ||
| self, | ||
| url: str, | ||
| *, | ||
| headers: Optional[_Headers] = None, | ||
| body: Optional[_Body] = None, | ||
| ) -> HttpResponse: | ||
| """Similar to :meth:`~.HttpClient.request` but performing a ``POST`` | ||
| request. | ||
| """ | ||
| return await self.request(url=url, method="POST", headers=headers, body=body) | ||
|
|
||
| async def execute(self, request: HttpRequest) -> HttpResponse: | ||
| """Accepts a single instance of :class:`~.HttpRequest` and executes it | ||
| using the request implementation configured in the :class:`~.HttpClient` | ||
| instance. | ||
|
|
||
| This returns a single :class:`~.HttpResponse`. | ||
| """ | ||
| return await self._request_downloader(request) | ||
|
|
||
| async def batch_execute( | ||
| self, *requests: HttpRequest, return_exceptions: bool = False | ||
| ) -> List[Union[HttpResponse, Exception]]: | ||
| """Similar to :meth:`~.HttpClient.execute` but accepts a collection of | ||
| :class:`~.HttpRequest` instances that would be batch executed. | ||
|
|
||
| The order of the :class:`~.HttpResponses` would correspond to the order | ||
| of :class:`~.HttpRequest` passed. | ||
|
|
||
| If any of the :class:`~.HttpRequest` raises an exception upon execution, | ||
| the exception is raised. | ||
|
|
||
| To prevent this, the actual exception can be returned alongside any | ||
| successful :class:`~.HttpResponse`. This enables salvaging any usable | ||
| responses despite any possible failures. This can be done by setting | ||
| ``True`` to the ``return_exceptions`` parameter. | ||
| """ | ||
|
|
||
| coroutines = [self._request_downloader(r) for r in requests] | ||
| responses = await asyncio.gather( | ||
| *coroutines, return_exceptions=return_exceptions | ||
| ) | ||
| return responses | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| class Meta(dict): | ||
| """Container class that could contain any arbitrary data to be passed into | ||
| a Page Object. | ||
|
|
||
| Note that this is simply a subclass of Python's ``dict``. | ||
| """ | ||
|
|
||
| pass |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to be inside this module, as moving it outside would cause a "circular import problem".
It's because
_perform_request()needsHttpRequestandHttpResponsefrom page_inputs for its annotations whileHttpClient(which is now inside the page_inputs subpackage) needs_perform_request().Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that can be possible to work around:
from __future__ import annotationsto the module which contains _perform_requestif typing.TYPE_CHECKING: from web_poet... import HttpRequest, HttpResponseSee https://mypy.readthedocs.io/en/stable/runtime_troubles.html#import-cycles and https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking
On a first sight, having this function in client.py looks fine though :) Do you see a better location for this function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @kmike , thanks for the suggestion! I was about to use your workaround but then simply importing
HttpRequestandHttpResponsein requests.py is now working without the "circular import problem". I think the problem before was a bad./toxcache in my local machine. 😅It would seem that the
_perform_request()function would be housed perfectly inweb_poet/requests.py. This makesweb_poet/page_inputs/client.pycleaner, containing code which emphasizes only on what users could use as page inputs.Updated this in a4f1dcc.