diff --git a/README.rst b/README.rst index 69c276d..b2dd88b 100644 --- a/README.rst +++ b/README.rst @@ -299,6 +299,33 @@ Session history for debugging s.history.latest.headers +Event hooks +------------------ + +.. code-block:: python + + # Another way to implement debugging is to use event hooks. + # The event hooks of the underlaying aiohttp or requests libraries can + # be used as such by passing them as event_hooks argument as a dict. + + # For example if you want to print all the sent data on console at async mode, you can use the + # 'on_request_chunk_sent' event hook https://docs.aiohttp.org/en/stable/tracing_reference.html#aiohttp.TraceConfig.on_request_chunk_sent + + import asyncio + async def sent(session, context, params): + print(f'sent {params.chunk}') + + s = Session( + 'http://0.0.0.0:8090/api', + enable_async=True, + schema=models_as_jsonschema, + event_hooks={'on_request_chunk_sent': sent} + ) + await s.get('some-collection') + await s.close() + + # On sychronous mode the available event hooks are listed here https://requests.readthedocs.io/en/master/user/advanced/#event-hooks + Credits ======= diff --git a/src/jsonapi_client/session.py b/src/jsonapi_client/session.py index 21f79a6..af6df25 100644 --- a/src/jsonapi_client/session.py +++ b/src/jsonapi_client/session.py @@ -196,7 +196,8 @@ def __init__(self, server_url: str=None, request_kwargs: dict=None, loop: 'AbstractEventLoop'=None, use_relationship_iterator: bool=False, - enable_history: bool=False) -> None: + enable_history: bool=False, + event_hooks: dict=None) -> None: self._server: ParseResult self.enable_async = enable_async @@ -214,11 +215,29 @@ def __init__(self, server_url: str=None, self.schema: Schema = Schema(schema) if enable_async: import aiohttp - self._aiohttp_session = aiohttp.ClientSession(loop=loop) + self._prepare_async_event_hooks(event_hooks) + self._aiohttp_session = aiohttp.ClientSession( + loop=loop, + trace_configs=[self.trace_config] + ) + else: + if event_hooks is not None: + hooks = self._request_kwargs.get('hooks', {}) + hooks.update(**event_hooks) + self._request_kwargs['hooks'] = hooks self.use_relationship_iterator = use_relationship_iterator self.enable_history = enable_history self.history = SessionHistory() + def _prepare_async_event_hooks(self, event_hooks: dict=None) -> None: + import aiohttp + self.trace_config = aiohttp.TraceConfig() + if event_hooks is None: + return + + for event, hook in event_hooks.items(): + getattr(self.trace_config, event).append(hook) + def add_resources(self, *resources: 'ResourceObject') -> None: """ Add resources to session cache. diff --git a/tests/test_client.py b/tests/test_client.py index fcf6cbd..4d61c33 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -3,7 +3,9 @@ from yarl import URL from aiohttp import ClientResponse +from aiohttp.client_exceptions import ServerDisconnectedError from aiohttp.helpers import TimerNoop +from aiohttp.test_utils import make_mocked_coro import jsonschema import pytest from requests import Response @@ -1908,7 +1910,7 @@ async def test_history_async_post(loop, session): 'http://localhost:8080', schema=api_schema_all, enable_async=True, - enable_history=True + enable_history=True, ) request_mock.return_value = response s.create('leases') @@ -1923,7 +1925,6 @@ async def test_history_async_post(loop, session): assert len(s.history) == 1 latest = s.history.latest - print(latest) assert latest.url == 'http://localhost:8080/leases' assert latest.http_method == 'POST' assert latest.send_json == { @@ -1943,3 +1944,77 @@ async def test_history_async_post(loop, session): latest.payload assert latest.content_length == len(response._body) assert latest.status_code == 500 + + +@pytest.mark.asyncio +async def test_on_request_chunk_sent_async_hook(): + data_sent = make_mocked_coro() + s = Session( + 'http://0.0.0.0:8080/api', + schema=api_schema_all, + enable_async=True, + enable_history=True, + event_hooks={'on_request_chunk_sent': data_sent} + ) + + s.create('leases') + a = s.create('leases') + assert a.is_dirty + a.lease_id = '1' + a.active_status = 'pending' + a.reference_number = 'test' + with pytest.raises(ServerDisconnectedError): + await a.commit() + assert data_sent.called + assert json.loads(data_sent.call_args[0][2].chunk) == { + 'data': { + 'attributes': { + 'active-status': 'pending', + 'lease-id': '1', + 'reference-number': 'test' + }, + 'relationships': {}, + 'type': 'leases' + } + } + + +def test_set_event_hooks_for_requests(): + """Event hooks for requests library is a keyword argument + so this test only tests that the request_kwargs are updated correctly. + """ + # Hooks not set at all + s = Session( + 'http://0.0.0.0:8080/api', + schema=api_schema_all + ) + assert 'hooks' not in s._request_kwargs + + # Hooks can be set from event_hooks + response_hook = Mock() + s = Session( + 'http://0.0.0.0:8080/api', + schema=api_schema_all, + event_hooks={'response': response_hook} + ) + assert 'hooks' in s._request_kwargs + assert s._request_kwargs.get('hooks') == {'response': response_hook} + + # Hooks can be set also from kwargs + s = Session( + 'http://0.0.0.0:8080/api', + schema=api_schema_all, + request_kwargs={'hooks': {'test': None}}, + event_hooks={'response': response_hook} + ) + assert 'hooks' in s._request_kwargs + assert s._request_kwargs.get('hooks') == {'response': response_hook, 'test': None} + + # Hooks set only at kwargs + s = Session( + 'http://0.0.0.0:8080/api', + schema=api_schema_all, + request_kwargs={'hooks': {'test': None}} + ) + assert 'hooks' in s._request_kwargs + assert s._request_kwargs.get('hooks') == {'test': None}