Skip to content

Commit

Permalink
Observer for http requests
Browse files Browse the repository at this point in the history
This change allows caller to observe technical details (e.g. headers,
body, etc.) of http requests that are sent in the background of odata
calls.
  • Loading branch information
mnezerka committed May 14, 2021
1 parent 94340c1 commit cc2386b
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 2 deletions.
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
# -- Project information -----------------------------------------------------

project = 'PyOData'
copyright = '2019 SAP SE or an SAP affiliate company'
copyright = '2021 SAP SE or an SAP affiliate company'
author = 'SAP'

# The short X.Y version
Expand Down
39 changes: 39 additions & 0 deletions docs/usage/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,42 @@ it is enough to set log level to the desired value.
logging.basicConfig()
root_logger = logging.getLogger()
root_logger.setLevel(logging.DEBUG)
Observing HTTP traffic
----------------------

There are cases where you need access to the transport protocol (http). For
example: you need to read value of specific http header. Pyodata provides
simple mechanism to observe all http requests and access low lever properties
directly from underlying engine (**python requests**).

You can use basic predefined observer class
``pyodata.utils.RequestObserverLastCall`` to catch last response headers:

.. code-block:: python
from pyodata.utils import RequestObserverLastCall
last = RequestObserverLastCall()
northwind.entity_sets.Employees.get_entity(1).execute(last)
print(last.response.headers)
You can also write your own observer to cover more specific cases. This is an example of
custom observer which stores status code of the last response.

.. code-block:: python
from pyodata.utils import RequestObserver
class CatchStatusCode(RequestObserver):
def __init__(self):
self.status_code = None
def http_response(self, response, request):
self.status_code = response.status_code
last_status = RequestObserverLastCall()
northwind.entity_sets.Employees.get_entity(1).execute(last_status)
print(last_status.status_code)
5 changes: 5 additions & 0 deletions pyodata/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Utilities for Python OData client"""

from .request_observer import RequestObserver, RequestObserverLastCall

__all__ = ["RequestObserver", "RequestObserverLastCall"]
34 changes: 34 additions & 0 deletions pyodata/utils/request_observer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""
Interface for request observer, which allows to catch odata request processing details
Author: Michal Nezerka <michal.nezerka@gmail.com>
Date: 2021-05-14
"""

from abc import ABC, abstractmethod


class RequestObserver(ABC):
"""
The RequestObserver interface declares methods for observing odata request processing.
"""

@abstractmethod
def http_response(self, response, request) -> None:
"""
Get http response together with related http request object.
"""


class RequestObserverLastCall(RequestObserver):
"""
The implementation of RequestObserver that stored request and response of the last call
"""

def __init__(self):
self.response = None
self.request = None

def http_response(self, response, request):
self.response = response
self.request = request
6 changes: 5 additions & 1 deletion pyodata/v2/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from urllib2 import quote

from pyodata.exceptions import HttpError, PyODataException, ExpressionError, ProgramError
from pyodata.utils import RequestObserver
from . import model

LOGGER_NAME = 'pyodata.service'
Expand Down Expand Up @@ -296,7 +297,7 @@ def add_headers(self, value):

self._headers.update(value)

def execute(self):
def execute(self, observer: RequestObserver = None):
"""Fetches HTTP response and returns processed result
Sends the query-request to the OData service, returning a client-side Enumerable for
Expand All @@ -320,6 +321,9 @@ def execute(self):
response = self._connection.request(
self.get_method(), url, headers=headers, params=params, data=body)

if observer:
observer.http_response(response, response.request)

self._logger.debug('Received response')
self._logger.debug(' url: %s', response.url)
self._logger.debug(' headers: %s', response.headers)
Expand Down
31 changes: 31 additions & 0 deletions tests/test_service_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import pytest
from unittest.mock import patch

from pyodata.utils import RequestObserverLastCall
import pyodata.v2.model
import pyodata.v2.service
from pyodata.exceptions import PyODataException, HttpError, ExpressionError, ProgramError
Expand Down Expand Up @@ -2372,3 +2373,33 @@ def test_custom_with_get_entity(service):

entity = service.entity_sets.MasterEntities.get_entity('12345').custom("foo", "bar").execute()
assert entity.Key == '12345'


@responses.activate
def test_request_observer(service):
"""Test use of request observer"""

responses.add(
responses.GET,
f"{service.url}/MasterEntities('12345')",
headers={'h1-key': 'h1-val', 'h2-key': 'h2-val'},
json={'d': {'Key': '12345'}},
status=200)

last = RequestObserverLastCall()
entity = service.entity_sets.MasterEntities.get_entity('12345').execute(last)

assert last.request is not None

assert len(last.request.headers) > 0
assert 'Accept' in last.request.headers

assert last.response is not None
assert last.response.status_code == 200
assert len(last.response.headers) == 3
assert 'Content-type' in last.response.headers
assert 'h1-key' in last.response.headers
assert 'h2-key' in last.response.headers
assert last.response.headers['Content-type'] == 'application/json'
assert last.response.headers['h1-key'] == 'h1-val'
assert last.response.headers['h2-key'] == 'h2-val'

0 comments on commit cc2386b

Please sign in to comment.