Skip to content

Commit f0da045

Browse files
authored
Merge pull request #24 from cloudblue/feature/logger
Add logger functionality for openapi client
2 parents 7887e25 + 9a09a00 commit f0da045

File tree

8 files changed

+180
-3
lines changed

8 files changed

+180
-3
lines changed

connect/client/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@
66
from connect.client.exceptions import ClientError # noqa
77
from connect.client.fluent import AsyncConnectClient, ConnectClient # noqa
88
from connect.client.rql import R # noqa
9+
from connect.client.logger import RequestLogger # noqa

connect/client/fluent.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def __init__(
2929
default_headers=None,
3030
default_limit=100,
3131
max_retries=0,
32+
logger=None,
3233
):
3334
"""
3435
Create a new instance of the ConnectClient.
@@ -42,6 +43,8 @@ def __init__(
4243
:type specs_location: str, optional
4344
:param default_headers: Http headers to apply to each request, defaults to {}
4445
:type default_headers: dict, optional
46+
:param logger: HTTPP Request logger class, defaults to None
47+
:type logger: RequestLogger, optional
4548
"""
4649
if default_headers and 'Authorization' in default_headers:
4750
raise ValueError('`default_headers` cannot contains `Authorization`')
@@ -58,6 +61,7 @@ def __init__(
5861
if self._use_specs:
5962
self.specs = OpenAPISpecs(self.specs_location)
6063
self.response = None
64+
self.logger = logger
6165
self._help_formatter = DefaultFormatter(self.specs)
6266

6367
def __getattr__(self, name):

connect/client/logger.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import json
2+
import sys
3+
4+
5+
class RequestLogger:
6+
def __init__(self, file=sys.stdout):
7+
self._file = file
8+
9+
def log_request(self, method, url, kwargs):
10+
other_args = {k: v for k, v in kwargs.items() if k not in ('headers', 'json', 'params')}
11+
12+
if 'params' in kwargs:
13+
url += '&' if '?' in url else '?'
14+
url += '&'.join([f'{k}={v}' for k, v in kwargs['params'].items()])
15+
16+
lines = [
17+
'--- HTTP Request ---',
18+
f'{method.upper()} {url} {other_args if other_args else ""}',
19+
]
20+
21+
if 'headers' in kwargs:
22+
for k, v in kwargs['headers'].items():
23+
lines.append(f'{k}: {v}')
24+
25+
if 'json' in kwargs:
26+
lines.append(json.dumps(kwargs['json'], indent=4))
27+
28+
lines.append('')
29+
30+
print(*lines, sep='\n', file=self._file)
31+
32+
def log_response(self, response):
33+
reason = response.raw.reason if getattr(response, 'raw', None) else response.reason_phrase
34+
lines = [
35+
'--- HTTP Response ---',
36+
f'{response.status_code} {reason}',
37+
]
38+
39+
for k, v in response.headers.items():
40+
lines.append(f'{k}: {v}')
41+
42+
if response.headers.get('Content-Type', None) == 'application/json':
43+
lines.append(json.dumps(response.json(), indent=4))
44+
45+
lines.append('')
46+
47+
print(*lines, sep='\n', file=self._file)

connect/client/mixins.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def execute(self, method, path, **kwargs):
5757

5858
try:
5959
self._execute_http_call(method, url, kwargs)
60+
6061
if self.response.status_code == 204:
6162
return None
6263
if self.response.headers['Content-Type'] == 'application/json':
@@ -72,7 +73,14 @@ def execute(self, method, path, **kwargs):
7273
def _execute_http_call(self, method, url, kwargs):
7374
retry_count = 0
7475
while True:
76+
if self.logger:
77+
self.logger.log_request(method, url, kwargs)
78+
7579
self.response = requests.request(method, url, **kwargs)
80+
81+
if self.logger:
82+
self.logger.log_response(self.response)
83+
7684
if ( # pragma: no branch
7785
self.response.status_code == 502
7886
and retry_count < self.max_retries
@@ -141,8 +149,15 @@ async def execute(self, method, path, **kwargs):
141149
async def _execute_http_call(self, method, url, kwargs):
142150
retry_count = 0
143151
while True:
152+
if self.logger:
153+
self.logger.log_request(method, url, kwargs)
154+
144155
async with httpx.AsyncClient() as client:
145156
self.response = await client.request(method, url, **kwargs)
157+
158+
if self.logger:
159+
self.logger.log_response(self.response)
160+
146161
if ( # pragma: no branch
147162
self.response.status_code == 502
148163
and retry_count < self.max_retries

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "connect-openapi-client"
3-
version = "21.0.0"
3+
version = "22.0.8"
44
description = "Connect Python OpenAPI Client"
55
authors = ["CloudBlue"]
66
license = "Apache-2.0"

tests/async_client/test_fluent.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import io
2+
13
import pytest
24

35
from connect.client import AsyncConnectClient, ClientError
6+
from connect.client.logger import RequestLogger
47
from connect.client.models import AsyncCollection, AsyncNS
58

69

@@ -89,7 +92,11 @@ async def test_execute(httpx_mock):
8992
json=expected,
9093
)
9194

92-
c = AsyncConnectClient('API_KEY', endpoint='https://localhost', use_specs=False)
95+
ios = io.StringIO()
96+
c = AsyncConnectClient('API_KEY',
97+
endpoint='https://localhost',
98+
use_specs=False,
99+
logger=RequestLogger(file=ios))
93100

94101
results = await c.execute('get', 'resources')
95102

tests/client/test_fluent.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import io
2+
13
import pytest
24

35
import responses
@@ -7,6 +9,7 @@
79
from connect.client.constants import CONNECT_ENDPOINT_URL, CONNECT_SPECS_URL
810
from connect.client.exceptions import ClientError
911
from connect.client.fluent import ConnectClient
12+
from connect.client.logger import RequestLogger
1013
from connect.client.models import Collection, NS
1114

1215

@@ -156,7 +159,11 @@ def test_execute(mocked_responses):
156159
json=expected,
157160
)
158161

159-
c = ConnectClient('API_KEY', endpoint='https://localhost', use_specs=False)
162+
ios = io.StringIO()
163+
c = ConnectClient('API_KEY',
164+
endpoint='https://localhost',
165+
use_specs=False,
166+
logger=RequestLogger(file=ios))
160167

161168
results = c.execute('get', 'resources')
162169

tests/client/test_logger.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import io
2+
3+
from requests.models import Response
4+
5+
from urllib3.response import HTTPResponse
6+
7+
from connect.client.logger import RequestLogger
8+
9+
10+
def test_log_request():
11+
LOG_REQUEST_HEADER = '--- HTTP Request ---\n'
12+
PATH1 = 'https://some.host.name/some/path'
13+
PATH2 = 'https://some.host.name/some/path?a=b'
14+
15+
ios = io.StringIO()
16+
rl = RequestLogger(file=ios)
17+
18+
rl.log_request('get', PATH1, {})
19+
assert ios.getvalue() == LOG_REQUEST_HEADER + 'GET ' + PATH1 + ' \n\n'
20+
21+
ios.truncate(0)
22+
ios.seek(0, 0)
23+
rl.log_request('get', PATH1, {'headers': {'Auth': 'None', 'Cookie': 'XXX'}})
24+
assert ios.getvalue() == LOG_REQUEST_HEADER + 'GET ' + PATH1 + ' \n' + """Auth: None
25+
Cookie: XXX
26+
27+
"""
28+
29+
ios.truncate(0)
30+
ios.seek(0, 0)
31+
rl.log_request('post', PATH1, {'json': {'id': 'XX-1234', 'name': 'XXX'}})
32+
assert ios.getvalue() == LOG_REQUEST_HEADER + 'POST ' + PATH1 + ' \n' + """{
33+
"id": "XX-1234",
34+
"name": "XXX"
35+
}
36+
37+
"""
38+
39+
ios.truncate(0)
40+
ios.seek(0, 0)
41+
rl.log_request('get', PATH1, {'params': {'limit': 10, 'offset': 0}})
42+
assert ios.getvalue() == LOG_REQUEST_HEADER + 'GET ' + PATH1 + '?limit=10&offset=0 \n\n'
43+
44+
ios.truncate(0)
45+
ios.seek(0, 0)
46+
rl.log_request('get', PATH2, {})
47+
assert ios.getvalue() == LOG_REQUEST_HEADER + 'GET ' + PATH2 + ' \n\n'
48+
49+
ios.truncate(0)
50+
ios.seek(0, 0)
51+
rl.log_request('get', PATH2, {'params': {'limit': 10, 'offset': 0}})
52+
assert ios.getvalue() == LOG_REQUEST_HEADER + 'GET ' + PATH2 + '&limit=10&offset=0 \n\n'
53+
54+
55+
def test_log_resposne(mocker):
56+
LOG_RESPONSE_HEADER = '--- HTTP Response ---\n'
57+
58+
ios = io.StringIO()
59+
rl = RequestLogger(file=ios)
60+
61+
rsp = Response()
62+
rsp.raw = HTTPResponse()
63+
64+
rsp.status_code = 200
65+
rsp.raw.reason = 'OK'
66+
rl.log_response(rsp)
67+
assert ios.getvalue() == LOG_RESPONSE_HEADER + '200 OK\n\n'
68+
69+
ios.truncate(0)
70+
ios.seek(0, 0)
71+
72+
rsp = Response()
73+
rsp.status_code = 200
74+
rsp.reason_phrase = 'OK'
75+
rl.log_response(rsp)
76+
assert ios.getvalue() == LOG_RESPONSE_HEADER + '200 OK\n\n'
77+
78+
ios.truncate(0)
79+
ios.seek(0, 0)
80+
81+
json = {'id': 'XX-1234', 'name': 'XXX'}
82+
mocker.patch('requests.models.Response.json', return_value=json)
83+
rsp = Response()
84+
rsp.raw = HTTPResponse()
85+
rsp.headers = {'Content-Type': 'application/json'}
86+
rsp.status_code = 200
87+
rsp.raw.reason = 'OK'
88+
rl.log_response(rsp)
89+
assert ios.getvalue() == LOG_RESPONSE_HEADER + """200 OK
90+
Content-Type: application/json
91+
{
92+
"id": "XX-1234",
93+
"name": "XXX"
94+
}
95+
96+
"""

0 commit comments

Comments
 (0)