Skip to content

Commit cc2386b

Browse files
committed
Observer for http requests
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.
1 parent 94340c1 commit cc2386b

File tree

6 files changed

+115
-2
lines changed

6 files changed

+115
-2
lines changed

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
# -- Project information -----------------------------------------------------
2121

2222
project = 'PyOData'
23-
copyright = '2019 SAP SE or an SAP affiliate company'
23+
copyright = '2021 SAP SE or an SAP affiliate company'
2424
author = 'SAP'
2525

2626
# The short X.Y version

docs/usage/advanced.rst

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,42 @@ it is enough to set log level to the desired value.
9595
logging.basicConfig()
9696
root_logger = logging.getLogger()
9797
root_logger.setLevel(logging.DEBUG)
98+
99+
Observing HTTP traffic
100+
----------------------
101+
102+
There are cases where you need access to the transport protocol (http). For
103+
example: you need to read value of specific http header. Pyodata provides
104+
simple mechanism to observe all http requests and access low lever properties
105+
directly from underlying engine (**python requests**).
106+
107+
You can use basic predefined observer class
108+
``pyodata.utils.RequestObserverLastCall`` to catch last response headers:
109+
110+
.. code-block:: python
111+
112+
from pyodata.utils import RequestObserverLastCall
113+
114+
last = RequestObserverLastCall()
115+
northwind.entity_sets.Employees.get_entity(1).execute(last)
116+
print(last.response.headers)
117+
118+
You can also write your own observer to cover more specific cases. This is an example of
119+
custom observer which stores status code of the last response.
120+
121+
.. code-block:: python
122+
123+
from pyodata.utils import RequestObserver
124+
125+
class CatchStatusCode(RequestObserver):
126+
127+
def __init__(self):
128+
self.status_code = None
129+
130+
def http_response(self, response, request):
131+
self.status_code = response.status_code
132+
133+
last_status = RequestObserverLastCall()
134+
135+
northwind.entity_sets.Employees.get_entity(1).execute(last_status)
136+
print(last_status.status_code)

pyodata/utils/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Utilities for Python OData client"""
2+
3+
from .request_observer import RequestObserver, RequestObserverLastCall
4+
5+
__all__ = ["RequestObserver", "RequestObserverLastCall"]

pyodata/utils/request_observer.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""
2+
Interface for request observer, which allows to catch odata request processing details
3+
4+
Author: Michal Nezerka <michal.nezerka@gmail.com>
5+
Date: 2021-05-14
6+
"""
7+
8+
from abc import ABC, abstractmethod
9+
10+
11+
class RequestObserver(ABC):
12+
"""
13+
The RequestObserver interface declares methods for observing odata request processing.
14+
"""
15+
16+
@abstractmethod
17+
def http_response(self, response, request) -> None:
18+
"""
19+
Get http response together with related http request object.
20+
"""
21+
22+
23+
class RequestObserverLastCall(RequestObserver):
24+
"""
25+
The implementation of RequestObserver that stored request and response of the last call
26+
"""
27+
28+
def __init__(self):
29+
self.response = None
30+
self.request = None
31+
32+
def http_response(self, response, request):
33+
self.response = response
34+
self.request = request

pyodata/v2/service.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from urllib2 import quote
2323

2424
from pyodata.exceptions import HttpError, PyODataException, ExpressionError, ProgramError
25+
from pyodata.utils import RequestObserver
2526
from . import model
2627

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

297298
self._headers.update(value)
298299

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

324+
if observer:
325+
observer.http_response(response, response.request)
326+
323327
self._logger.debug('Received response')
324328
self._logger.debug(' url: %s', response.url)
325329
self._logger.debug(' headers: %s', response.headers)

tests/test_service_v2.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import pytest
77
from unittest.mock import patch
88

9+
from pyodata.utils import RequestObserverLastCall
910
import pyodata.v2.model
1011
import pyodata.v2.service
1112
from pyodata.exceptions import PyODataException, HttpError, ExpressionError, ProgramError
@@ -2372,3 +2373,33 @@ def test_custom_with_get_entity(service):
23722373

23732374
entity = service.entity_sets.MasterEntities.get_entity('12345').custom("foo", "bar").execute()
23742375
assert entity.Key == '12345'
2376+
2377+
2378+
@responses.activate
2379+
def test_request_observer(service):
2380+
"""Test use of request observer"""
2381+
2382+
responses.add(
2383+
responses.GET,
2384+
f"{service.url}/MasterEntities('12345')",
2385+
headers={'h1-key': 'h1-val', 'h2-key': 'h2-val'},
2386+
json={'d': {'Key': '12345'}},
2387+
status=200)
2388+
2389+
last = RequestObserverLastCall()
2390+
entity = service.entity_sets.MasterEntities.get_entity('12345').execute(last)
2391+
2392+
assert last.request is not None
2393+
2394+
assert len(last.request.headers) > 0
2395+
assert 'Accept' in last.request.headers
2396+
2397+
assert last.response is not None
2398+
assert last.response.status_code == 200
2399+
assert len(last.response.headers) == 3
2400+
assert 'Content-type' in last.response.headers
2401+
assert 'h1-key' in last.response.headers
2402+
assert 'h2-key' in last.response.headers
2403+
assert last.response.headers['Content-type'] == 'application/json'
2404+
assert last.response.headers['h1-key'] == 'h1-val'
2405+
assert last.response.headers['h2-key'] == 'h2-val'

0 commit comments

Comments
 (0)