From f9244b2cf7700bbd44e2563ae88f30583e78db60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20Nyk=C3=A4nen?= Date: Sun, 8 Mar 2020 19:32:19 +0200 Subject: [PATCH 1/8] #28: Add session history to ease request debugging --- README.rst | 36 ++++++++ src/jsonapi_client/session.py | 88 +++++++++++++++++- tests/test_client.py | 166 ++++++++++++++++++++++++++++++++++ 3 files changed, 289 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 7291387..90caf19 100644 --- a/README.rst +++ b/README.rst @@ -273,6 +273,42 @@ Deleting resources cust1.commit() # Actually delete +Session history for debugging +------------------ + +.. code-block:: python + + # You can use session history to debug http requests. + # Session history will be enabled, if you set the log level to DEBUG + # or explicitly set a session parameter enable_history_at_log level to a log level + # equal or lower than the actual log level. + + # Session history will be enabled by: (Python log level is WARNING by default) + s = Session('http://localhost:8080/', schema=models_as_jsonschema, + enable_history_at_loglevel='WARNING') + # or + import logging + logging.basicConfig(level='DEBUG') + s = Session('http://localhost:8080/', schema=models_as_jsonschema) + + # Session history will be disabled + logging.basicConfig(level='INFO') + s = Session('http://localhost:8080/', schema=models_as_jsonschema) + + # Session history is a list of session history items. + # You can see the information about the request and response + # For example + s.history.latest + # will print out some data about the latest request + # That actually equals to + s.history[-1] + + # You can see the latest server response by + print(s.history.latest.response_content) + # or to see the response headers + s.history.latest.headers + + Credits ======= diff --git a/src/jsonapi_client/session.py b/src/jsonapi_client/session.py index 4732c95..41e5c3e 100644 --- a/src/jsonapi_client/session.py +++ b/src/jsonapi_client/session.py @@ -56,6 +56,78 @@ NOT_FOUND = object() +class SessionHistory(list): + @property + def latest(self): + if len(self): + return self[-1] + else: + return None + + +class SessionHistoryItem: + def __init__(self, session: 'Session', url: str, http_method: str, response, send_json: dict=None): + self.session = session + self.response = response + self.url = url + self.send_json = send_json + self.http_method = http_method.upper() + + def __repr__(self): + content = self.content + content_cut_after = 100 + if len(content) > content_cut_after: + content = f"{content[:content_cut_after]}..." + request_str = f"Request: {self.url}\n method: {self.http_method}\n" + if self.send_json: + request_content = json.dumps(self.send_json) + if len(request_content) > content_cut_after: + request_content = f"{request_content[:content_cut_after]}..." + request_str += f" payload: {request_content}\n" + r = f"{request_str}" \ + f"Response: \n status code: {self.status_code}\n" \ + f" content length: {self.content_length}\n" \ + f" content: {content}" + return r + + @property + def content(self): + if self.session.enable_async: + return self.response._body + else: + return self.response.content + + @property + def response_content(self): + """ + This is used to pretty print the contents for debugging purposes. + If you don't want pretty print, please use self.response.content directly + Example: If session is s, you can pretty print out the latest content by + print(s.history.latest.content) + """ + loaded = json.loads(self.response.content) + return json.dumps(loaded, indent=4, sort_keys=True) + + @property + def payload(self): + return json.dumps(self.send_json, indent=4, sort_keys=True) + + @property + def content_length(self): + return len(self.content) + + @property + def headers(self): + return self.response.headers + + @property + def status_code(self): + if self.session.enable_async: + return self.response.status + else: + return self.response.status_code + + class Schema: """ Container for model schemas with associated methods. @@ -123,7 +195,8 @@ def __init__(self, server_url: str=None, schema: dict=None, request_kwargs: dict=None, loop: 'AbstractEventLoop'=None, - use_relationship_iterator: bool=False,) -> None: + use_relationship_iterator: bool=False, + enable_history_at_loglevel: str='DEBUG') -> None: self._server: ParseResult self.enable_async = enable_async @@ -143,6 +216,8 @@ def __init__(self, server_url: str=None, import aiohttp self._aiohttp_session = aiohttp.ClientSession(loop=loop) self.use_relationship_iterator = use_relationship_iterator + self._enable_history = logger.isEnabledFor(getattr(logging, enable_history_at_loglevel)) + self.history = SessionHistory() def add_resources(self, *resources: 'ResourceObject') -> None: """ @@ -475,6 +550,13 @@ async def _ext_fetch_by_url_async(self, url: str) -> 'Document': json_data = await self._fetch_json_async(url) return self.read(json_data, url) + def _append_to_session_history(self, url: str, http_method: str, + response, send_json: dict=None): + if self._enable_history: + self.history.append( + SessionHistoryItem(self, url, http_method, response, send_json) + ) + def _fetch_json(self, url: str) -> dict: """ Internal use. @@ -487,6 +569,7 @@ def _fetch_json(self, url: str) -> dict: logger.info('Fetching document from url %s', parsed_url) response = requests.get(parsed_url.geturl(), **self._request_kwargs) response_content = response.json() + self._append_to_session_history(url, 'GET', response) if response.status_code == HttpStatus.OK_200: return response_content else: @@ -508,6 +591,7 @@ async def _fetch_json_async(self, url: str) -> dict: async with self._aiohttp_session.get(parsed_url.geturl(), **self._request_kwargs) as response: response_content = await response.json(content_type='application/vnd.api+json') + self._append_to_session_history(url, 'GET', response) if response.status == HttpStatus.OK_200: return response_content else: @@ -536,6 +620,7 @@ def http_request(self, http_method: str, url: str, send_json: dict, **kwargs) response_json = response.json() + self._append_to_session_history(url, http_method, response, send_json) if response.status_code not in expected_statuses: raise DocumentError(f'Could not {http_method.upper()} ' f'({response.status_code}): ' @@ -574,6 +659,7 @@ async def http_request_async( **kwargs) as response: response_json = await response.json(content_type=content_type) + self._append_to_session_history(url, http_method, response, send_json) if response.status not in expected_statuses: raise DocumentError(f'Could not {http_method.upper()} ' f'({response.status}): ' diff --git a/tests/test_client.py b/tests/test_client.py index ba08d07..6d27d07 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1762,3 +1762,169 @@ async def test_error_handling_posting_async(loop, session): assert str(exp.value) == 'Could not POST (500): Internal server error' patcher.stop() + + +def test_history_get(): + response = Response() + response.url = URL('http://localhost:8080/leases') + response.request = mock.Mock() + response.headers = {'Content-Type': 'application/vnd.api+json'} + response._content = json.dumps({'data': []}).encode('UTF-8') + response.status_code = 200 + + patcher = mock.patch('requests.get') + client_mock = patcher.start() + # Session history will be disabled, if not explicitly enabled + # or log level set to DEBUG. + # Python loglevel is WARNING by default. + s = Session( + 'http://localhost:8080', + schema=api_schema_all, + ) + client_mock.return_value = response + s.get('leases') + assert len(s.history) == 0 + + s = Session( + 'http://localhost:8080', + schema=api_schema_all, + enable_history_at_loglevel='WARNING' + ) + s.get('leases') + assert len(s.history) == 1 + assert s.history.latest == s.history[-1] + latest = s.history.latest + assert latest.url == 'http://localhost:8080/leases' + assert latest.http_method == 'GET' + assert latest.send_json is None + assert latest.content_length == len(response._content) + assert latest.status_code == 200 + + +def test_history_post(): + response = Response() + response.url = URL('http://localhost:8080/invalid') + response.request = mock.Mock() + response.headers = {'Content-Type': 'application/vnd.api+json'} + response._content = json.dumps( + {'errors': [{'title': 'Internal server error'}]} + ).encode('UTF-8') + response.status_code = 500 + + patcher = mock.patch('requests.request') + client_mock = patcher.start() + s = Session('http://localhost:8080', schema=leases, enable_history_at_loglevel='WARNING') + client_mock.return_value = response + a = s.create('leases') + assert a.is_dirty + a.lease_id = '1' + a.active_status = 'pending' + a.reference_number = 'test' + with pytest.raises(DocumentError): + a.commit() + + assert len(s.history) == 1 + latest = s.history.latest + assert latest.url == 'http://localhost:8080/leases' + assert latest.http_method == 'POST' + assert latest.send_json == { + 'data': { + 'attributes': { + 'active-status': 'pending', + 'lease-id': '1', + 'reference-number': 'test' + }, + 'relationships': {}, + 'type': 'leases' + } + } + assert latest.content_length == len(response._content) + assert latest.status_code == 500 + + +@pytest.mark.asyncio +async def test_history_async_get(loop, session): + response = ClientResponse('get', URL('http://localhost/invalid'), + request_info=mock.Mock(), + writer=mock.Mock(), + continue100=None, + timer=TimerNoop(), + traces=[], + loop=loop, + session=session, + ) + response._headers = {'Content-Type': 'application/vnd.api+json'} + response._body = json.dumps({'errors': [{'title': 'Resource not found'}]}).encode('UTF-8') + response.status = 404 + + patcher = mock.patch('aiohttp.ClientSession') + client_mock = patcher.start() + s = Session( + 'http://localhost', schema=leases, enable_async=True, enable_history_at_loglevel='WARNING' + ) + client_mock().get.return_value = response + with pytest.raises(DocumentError): + await s.get('invalid') + + patcher.stop() + + assert len(s.history) == 1 + latest = s.history.latest + assert latest.url == 'http://localhost/invalid' + assert latest.http_method == 'GET' + assert latest.send_json is None + assert latest.content_length == len(response._body) + assert latest.status_code == 404 + + +@pytest.mark.asyncio +async def test_history_async_post(loop, session): + response = ClientResponse('post', URL('http://localhost:8080/leases'), + request_info=mock.Mock(), + writer=mock.Mock(), + continue100=None, + timer=TimerNoop(), + traces=[], + loop=loop, + session=session, + ) + response._headers = {'Content-Type': 'application/vnd.api+json'} + response._body = json.dumps({'errors': [{'title': 'Internal server error'}]}).encode('UTF-8') + response.status = 500 + + patcher = mock.patch('aiohttp.ClientSession.request') + request_mock = patcher.start() + s = Session( + 'http://localhost:8080', + schema=api_schema_all, + enable_async=True, + enable_history_at_loglevel='WARNING' + ) + request_mock.return_value = response + 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(DocumentError): + await a.commit() + patcher.stop() + + assert len(s.history) == 1 + latest = s.history.latest + assert latest.url == 'http://localhost:8080/leases' + assert latest.http_method == 'POST' + assert latest.send_json == { + 'data': { + 'attributes': { + 'active-status': 'pending', + 'lease-id': '1', + 'reference-number': 'test' + }, + 'relationships': {}, + 'type': 'leases' + } + } + assert latest.content_length == len(response._body) + assert latest.status_code == 500 From 073c240e7aef967147e6e4062a10885b4145c7a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20Nyk=C3=A4nen?= Date: Sun, 8 Mar 2020 19:43:34 +0200 Subject: [PATCH 2/8] Add history item print for __repr__ test --- tests/test_client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_client.py b/tests/test_client.py index 6d27d07..f58c717 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1913,6 +1913,7 @@ 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 == { From da779fb78932349dfbeea6157b09a65f9737ef21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20Nyk=C3=A4nen?= Date: Sun, 8 Mar 2020 20:10:03 +0200 Subject: [PATCH 3/8] Fix session history item response_content property --- src/jsonapi_client/session.py | 2 +- tests/test_client.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/jsonapi_client/session.py b/src/jsonapi_client/session.py index 41e5c3e..caf674a 100644 --- a/src/jsonapi_client/session.py +++ b/src/jsonapi_client/session.py @@ -105,7 +105,7 @@ def response_content(self): Example: If session is s, you can pretty print out the latest content by print(s.history.latest.content) """ - loaded = json.loads(self.response.content) + loaded = json.loads(self.content) return json.dumps(loaded, indent=4, sort_keys=True) @property diff --git a/tests/test_client.py b/tests/test_client.py index f58c717..85df287 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1797,6 +1797,10 @@ def test_history_get(): assert latest.url == 'http://localhost:8080/leases' assert latest.http_method == 'GET' assert latest.send_json is None + assert 'Content-Type' in latest.headers + # Just make sure that these properties execute + latest.response_content + latest.payload assert latest.content_length == len(response._content) assert latest.status_code == 200 @@ -1838,6 +1842,10 @@ def test_history_post(): 'type': 'leases' } } + assert 'Content-Type' in latest.headers + # Just make sure that these properties execute + latest.response_content + latest.payload assert latest.content_length == len(response._content) assert latest.status_code == 500 @@ -1873,6 +1881,10 @@ async def test_history_async_get(loop, session): assert latest.url == 'http://localhost/invalid' assert latest.http_method == 'GET' assert latest.send_json is None + assert 'Content-Type' in latest.headers + # Just make sure that these properties execute + latest.response_content + latest.payload assert latest.content_length == len(response._body) assert latest.status_code == 404 @@ -1927,5 +1939,9 @@ async def test_history_async_post(loop, session): 'type': 'leases' } } + assert 'Content-Type' in latest.headers + # Just make sure that these properties execute + latest.response_content + latest.payload assert latest.content_length == len(response._body) assert latest.status_code == 500 From f85a70d16926964e79ed79f79877c6c93614e078 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20Nyk=C3=A4nen?= Date: Sat, 14 Mar 2020 08:28:00 +0200 Subject: [PATCH 4/8] Session history can be enabled only by an optional session parameter --- README.rst | 14 ++------------ src/jsonapi_client/session.py | 6 +++--- tests/test_client.py | 10 ++++------ 3 files changed, 9 insertions(+), 21 deletions(-) diff --git a/README.rst b/README.rst index 90caf19..69c276d 100644 --- a/README.rst +++ b/README.rst @@ -279,21 +279,11 @@ Session history for debugging .. code-block:: python # You can use session history to debug http requests. - # Session history will be enabled, if you set the log level to DEBUG - # or explicitly set a session parameter enable_history_at_log level to a log level - # equal or lower than the actual log level. + # Session history will be enabled by initialising session with enable_history=True parameter. # Session history will be enabled by: (Python log level is WARNING by default) s = Session('http://localhost:8080/', schema=models_as_jsonschema, - enable_history_at_loglevel='WARNING') - # or - import logging - logging.basicConfig(level='DEBUG') - s = Session('http://localhost:8080/', schema=models_as_jsonschema) - - # Session history will be disabled - logging.basicConfig(level='INFO') - s = Session('http://localhost:8080/', schema=models_as_jsonschema) + enable_history=True) # Session history is a list of session history items. # You can see the information about the request and response diff --git a/src/jsonapi_client/session.py b/src/jsonapi_client/session.py index caf674a..21f79a6 100644 --- a/src/jsonapi_client/session.py +++ b/src/jsonapi_client/session.py @@ -196,7 +196,7 @@ def __init__(self, server_url: str=None, request_kwargs: dict=None, loop: 'AbstractEventLoop'=None, use_relationship_iterator: bool=False, - enable_history_at_loglevel: str='DEBUG') -> None: + enable_history: bool=False) -> None: self._server: ParseResult self.enable_async = enable_async @@ -216,7 +216,7 @@ def __init__(self, server_url: str=None, import aiohttp self._aiohttp_session = aiohttp.ClientSession(loop=loop) self.use_relationship_iterator = use_relationship_iterator - self._enable_history = logger.isEnabledFor(getattr(logging, enable_history_at_loglevel)) + self.enable_history = enable_history self.history = SessionHistory() def add_resources(self, *resources: 'ResourceObject') -> None: @@ -552,7 +552,7 @@ async def _ext_fetch_by_url_async(self, url: str) -> 'Document': def _append_to_session_history(self, url: str, http_method: str, response, send_json: dict=None): - if self._enable_history: + if self.enable_history: self.history.append( SessionHistoryItem(self, url, http_method, response, send_json) ) diff --git a/tests/test_client.py b/tests/test_client.py index 85df287..fcf6cbd 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1775,8 +1775,6 @@ def test_history_get(): patcher = mock.patch('requests.get') client_mock = patcher.start() # Session history will be disabled, if not explicitly enabled - # or log level set to DEBUG. - # Python loglevel is WARNING by default. s = Session( 'http://localhost:8080', schema=api_schema_all, @@ -1788,7 +1786,7 @@ def test_history_get(): s = Session( 'http://localhost:8080', schema=api_schema_all, - enable_history_at_loglevel='WARNING' + enable_history=True ) s.get('leases') assert len(s.history) == 1 @@ -1817,7 +1815,7 @@ def test_history_post(): patcher = mock.patch('requests.request') client_mock = patcher.start() - s = Session('http://localhost:8080', schema=leases, enable_history_at_loglevel='WARNING') + s = Session('http://localhost:8080', schema=leases, enable_history=True) client_mock.return_value = response a = s.create('leases') assert a.is_dirty @@ -1868,7 +1866,7 @@ async def test_history_async_get(loop, session): patcher = mock.patch('aiohttp.ClientSession') client_mock = patcher.start() s = Session( - 'http://localhost', schema=leases, enable_async=True, enable_history_at_loglevel='WARNING' + 'http://localhost', schema=leases, enable_async=True, enable_history=True ) client_mock().get.return_value = response with pytest.raises(DocumentError): @@ -1910,7 +1908,7 @@ async def test_history_async_post(loop, session): 'http://localhost:8080', schema=api_schema_all, enable_async=True, - enable_history_at_loglevel='WARNING' + enable_history=True ) request_mock.return_value = response s.create('leases') From 52ed3f674fc62c0813b6638f7d37e858d378bed3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20Nyk=C3=A4nen?= Date: Mon, 16 Mar 2020 10:30:42 +0200 Subject: [PATCH 5/8] #28: Add a possibility to use event hooks of aiohttp and requests libraries --- README.rst | 27 ++++++++++++ src/jsonapi_client/session.py | 23 +++++++++- tests/test_client.py | 79 ++++++++++++++++++++++++++++++++++- 3 files changed, 125 insertions(+), 4 deletions(-) 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} From a334a31b635b531a525ef59927a88abaa66eaf7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20Nyk=C3=A4nen?= Date: Mon, 16 Mar 2020 18:56:55 +0200 Subject: [PATCH 6/8] Fix for failing test --- tests/test_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_client.py b/tests/test_client.py index 4d61c33..1dd7d63 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1950,7 +1950,7 @@ async def test_history_async_post(loop, session): async def test_on_request_chunk_sent_async_hook(): data_sent = make_mocked_coro() s = Session( - 'http://0.0.0.0:8080/api', + 'http://localhost/api', schema=api_schema_all, enable_async=True, enable_history=True, From e6ac44582b65f433719613b5157cc3c3db646c90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20Nyk=C3=A4nen?= Date: Mon, 16 Mar 2020 19:00:09 +0200 Subject: [PATCH 7/8] Fix for failing test --- tests/test_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_client.py b/tests/test_client.py index 1dd7d63..e7d1ea0 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1963,7 +1963,7 @@ async def test_on_request_chunk_sent_async_hook(): a.lease_id = '1' a.active_status = 'pending' a.reference_number = 'test' - with pytest.raises(ServerDisconnectedError): + with pytest.raises(ConnectionRefusedError): await a.commit() assert data_sent.called assert json.loads(data_sent.call_args[0][2].chunk) == { From 697db9e4dec79f706d3711cb2949f711e10e6ad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20Nyk=C3=A4nen?= Date: Tue, 17 Mar 2020 09:12:12 +0200 Subject: [PATCH 8/8] Refactor event tests to use AioHTTPTestCase --- tests/test_client.py | 111 +++++++++++++++++++++++++++++-------------- 1 file changed, 75 insertions(+), 36 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index e7d1ea0..804c1d7 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -2,10 +2,9 @@ from urllib.parse import urlparse from yarl import URL -from aiohttp import ClientResponse -from aiohttp.client_exceptions import ServerDisconnectedError +from aiohttp import ClientResponse, web from aiohttp.helpers import TimerNoop -from aiohttp.test_utils import make_mocked_coro +from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop, make_mocked_coro import jsonschema import pytest from requests import Response @@ -1946,39 +1945,6 @@ async def test_history_async_post(loop, session): 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://localhost/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(ConnectionRefusedError): - 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. @@ -2018,3 +1984,76 @@ def test_set_event_hooks_for_requests(): ) assert 'hooks' in s._request_kwargs assert s._request_kwargs.get('hooks') == {'test': None} + + +class TestEventHooks(AioHTTPTestCase): + async def get_application(self): + async def leases_create(request): + headers = {'Content-Type': 'application/vnd.api+json'} + data = {'errors': [{'title': 'Internal server error'}]} + data = json.dumps(data) + return web.Response(body=data, status=500, headers=headers) + + app = web.Application() + app.router.add_post('/api/leases', leases_create) + return app + + @unittest_run_loop + async def test_on_request_chunk_sent_async_hook(self): + data_sent = make_mocked_coro() + + url = f'http://{self.server.host}:{self.server.port}/api' + s = Session( + url, + 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(DocumentError): + 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' + } + } + + @unittest_run_loop + async def test_on_response_chunk_received_async_hook(self): + data_received = make_mocked_coro() + + url = f'http://{self.server.host}:{self.server.port}/api' + s = Session( + url, + schema=api_schema_all, + enable_async=True, + enable_history=True, + event_hooks={'on_response_chunk_received': data_received} + ) + + 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(DocumentError): + await a.commit() + assert data_received.called + assert json.loads(data_received.call_args[0][2].chunk) == { + 'errors': [{'title': 'Internal server error'}] + }