Skip to content

Commit 22e2b2e

Browse files
Merge pull request #137 from avadev/AS-147
PR : AS-147 : Added logging to Python V2 SDK
2 parents 8a63b9e + ee6b012 commit 22e2b2e

File tree

2 files changed

+115
-11
lines changed

2 files changed

+115
-11
lines changed

src/avalara/ava_logger.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import functools
2+
import logging
3+
import json
4+
import time
5+
6+
import requests
7+
8+
from . import client
9+
10+
11+
12+
def ava_log(func):
13+
"""
14+
Function decorator for implementation of logging
15+
@param func: function where logging needs to be implemented
16+
@return: function
17+
"""
18+
19+
@functools.wraps(func)
20+
def wrapper(*args, **kwargs):
21+
logger = logging.getLogger()
22+
is_log_req_resp_allowed = False
23+
ava_log_entry = {}
24+
is_error_log = False
25+
26+
# Logic :
27+
# 1) Check if decorator is called for function in AvaTaxClient class
28+
# 2) If logger is set then use it else use new instance of logger (this property is useful when SDK consumer
29+
# wants to use specific configuration)
30+
# 3) Execute actual method and create log entry in case of http response only
31+
# 4) In case of exception though log error
32+
execution_start_time = time.perf_counter()
33+
try:
34+
if isinstance(args[0], client.AvataxClient):
35+
if hasattr(args[0], "logger") and (args[0].__getattribute__("logger") is not None):
36+
logger = args[0].__getattribute__("logger")
37+
if hasattr(args[0], "is_log_req_resp_allowed"):
38+
is_log_req_resp_allowed = args[0].__getattribute__("is_log_req_resp_allowed")
39+
result = func(*args, **kwargs)
40+
if result is not None and isinstance(result, requests.models.Response):
41+
ava_log_entry = get_ava_log_entry(result, is_log_req_resp_allowed)
42+
return result
43+
except Exception as e:
44+
is_error_log = True
45+
ava_log_entry["error"] = str(e)
46+
raise e
47+
finally:
48+
total_execution_time = time.perf_counter() - execution_start_time
49+
if ava_log_entry["execution_time"] is None:
50+
ava_log_entry["execution_time"] = total_execution_time * 1000
51+
json_data = json.dumps(ava_log_entry, indent=4)
52+
if is_error_log:
53+
logger.error(json_data)
54+
else:
55+
logger.info(json_data)
56+
57+
return wrapper
58+
59+
60+
def get_ava_log_entry(result: requests.Response, is_log_req_resp_allowed: bool) -> dict:
61+
log_entry = {}
62+
log_entry["execution_time"] = result.elapsed.total_seconds() * 1000
63+
log_entry["x-correlation-id"] = result.headers["x-correlation-id"]
64+
log_entry["status_code"] = result.status_code
65+
log_entry["request_url"] = result.url
66+
log_entry["method"] = result.request.method
67+
if is_log_req_resp_allowed:
68+
log_entry["request"] = str(result.request.body)
69+
log_entry["response"] = result.text
70+
71+
return log_entry
72+
73+
74+
def decorate_all_methods(decorator, exclude=["__init__", "add_credentials"]):
75+
"""
76+
Decorator to be applied at class level. This decorator decorates all methods in class with
77+
supplied decorator in parameter.
78+
@param decorator: decorator to be applied to all methods in class
79+
@param exclude: list of methods to exclude from being decorated.
80+
@return: class with decorated methods
81+
"""
82+
83+
def decorate(cls):
84+
for attr in cls.__dict__:
85+
if callable(getattr(cls, attr)) and attr not in exclude:
86+
setattr(cls, attr, decorator(getattr(cls, attr)))
87+
return cls
88+
89+
return decorate

src/avalara/client.py

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,26 @@
2424
@version TBD
2525
@link https://github.com/avadev/AvaTax-REST-V2-Python-SDK
2626
"""
27+
import logging
2728
from requests.auth import HTTPBasicAuth
28-
from ._str_version import str_type
29+
2930
from . import client_methods
30-
import os
31+
from ._str_version import str_type
3132

3233

3334
class AvataxClient(client_methods.Mixin):
3435
"""Class for our Avatax client."""
3536

36-
def __init__(self, app_name=None, app_version=None, machine_name=None,
37-
environment=None, timeout_limit=None):
37+
def __init__(
38+
self,
39+
app_name=None,
40+
app_version=None,
41+
machine_name=None,
42+
environment=None,
43+
timeout_limit=None,
44+
is_log_req_resp_allowed=False,
45+
logger=None
46+
):
3847
"""
3948
Initialize the sandbox client.
4049
@@ -47,11 +56,13 @@ def __init__(self, app_name=None, app_version=None, machine_name=None,
4756
:param string environment: Default environment is production,
4857
input sandbox, for the sandbox API
4958
:param int/float The timeout limit for every call made by this client instance. (default: 10 sec)
59+
:param bool is_log_req_resp_allowed: is logging request and response is allowed (default: False)
5060
:return: object
5161
"""
52-
if not all(isinstance(i, str_type) for i in [app_name,
53-
machine_name,
54-
environment]):
62+
if not all(
63+
isinstance(i, str_type)
64+
for i in [app_name, machine_name, environment]
65+
):
5566
raise ValueError('Input(s) must be string or none type object')
5667
self.base_url = 'https://rest.avatax.com'
5768
if environment:
@@ -63,11 +74,15 @@ def __init__(self, app_name=None, app_version=None, machine_name=None,
6374
self.app_name = app_name
6475
self.app_version = app_version
6576
self.machine_name = machine_name
66-
self.client_id = '{}; {}; Python SDK; API_VERSION; {};'.format(app_name,
67-
app_version,
68-
machine_name)
77+
self.client_id = '{}; {}; Python SDK; API_VERSION; {};'.format(
78+
app_name, app_version, machine_name
79+
)
6980
self.client_header = {'X-Avalara-Client': self.client_id}
70-
self.timeout_limit = timeout_limit
81+
self.timeout_limit = timeout_limit
82+
self.is_log_req_resp_allowed = is_log_req_resp_allowed
83+
self.logger = logger
84+
# if self.logger is set, logging is done using supplied logger configuration
85+
7186

7287
def add_credentials(self, username=None, password=None):
7388
"""

0 commit comments

Comments
 (0)