Skip to content
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

#28: Add session history to ease request debugging #47

Open
wants to merge 8 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,59 @@ 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 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=True)

# 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


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
=======

Expand Down
109 changes: 107 additions & 2 deletions src/jsonapi_client/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.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.
Expand Down Expand Up @@ -123,7 +195,9 @@ 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: bool=False,
event_hooks: dict=None) -> None:
self._server: ParseResult
self.enable_async = enable_async

Expand All @@ -141,8 +215,28 @@ 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:
"""
Expand Down Expand Up @@ -475,6 +569,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.
Expand All @@ -487,6 +588,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:
Expand All @@ -508,6 +610,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:
Expand Down Expand Up @@ -536,6 +639,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}): '
Expand Down Expand Up @@ -574,6 +678,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}): '
Expand Down
Loading