1- import json , hashlib , requests
2- from typing import Any , Dict , List , Optional , Self
1+ import json , hashlib , requests , requests_cache
2+ import urllib .parse
3+ from typing import Any , Dict , List , Literal , Optional , Self
34from datetime import datetime , timedelta , timezone
45from ._exceptions import SarvException
56from ._mixins import ModulesMixin
@@ -21,6 +22,8 @@ def __init__(
2122 login_type : Optional [str ] = None ,
2223 language : SarvLanguageType = 'en_US' ,
2324 is_password_md5 : bool = False ,
25+ caching : bool = False ,
26+ cache_backend : Literal ['memory' , 'sqlite' ] = 'memory' ,
2427 ) -> None :
2528 """
2629 Initialize the SarvClient.
@@ -34,35 +37,29 @@ def __init__(
3437 language (SarvLanguageType): The language to use, default is 'en_US'.
3538 is_password_md5 (bool): Whether the password is already hashed using MD5.
3639 """
37- self .url = url
38- self .utype = utype
39- self .username = username
40- self .password = password if is_password_md5 else self .hash_password (password )
41- self .login_type = login_type
42- self .language = language
43-
44- self .token : str = ''
45- self ._session = requests .session ()
46- self ._session .headers .update ({'Content-Type' : 'application/json' })
47- self ._session .headers .update ({'Accept' : 'application/json' })
40+ self ._url = url
41+ self ._utype = utype
42+ self ._username = username
43+ self ._password = password if is_password_md5 else self .hash_password (password )
44+ self ._login_type = login_type
45+ self ._language = language
46+ self ._caching = caching
47+ self ._cache_backend = cache_backend
4848
49- super (). __init__ ()
49+ self . _token : str = ''
5050
51+ self .enable_caching () if self ._caching else self .disable_caching ()
5152
52- @staticmethod
53- def hash_password (password : str ) -> str :
54- """
55- Returns the acceptable hash for SarvCRM Login
53+ super ().__init__ ()
5654
57- Args:
58- password(str): your password
59-
60- Returns:
61- str: md5 hashed password
55+ def _add_headers (self ) -> None :
6256 """
63- return hashlib .md5 (password .encode ('utf-8' )).hexdigest ()
57+ Adds required sarvcrm headers to session.
58+ """
59+ self ._session .headers .update ({'Content-Type' : 'application/json' })
60+ self ._session .headers .update ({'Accept' : 'application/json' })
6461
65- def create_get_params (
62+ def _create_get_params (
6663 self ,
6764 sarv_get_method : Optional [SarvGetMethods ] = None ,
6865 sarv_module : Optional [SarvModule | str ] = None ,
@@ -88,7 +85,7 @@ def create_get_params(
8885 module_name = sarv_module
8986 else :
9087 raise TypeError (f'Module type must be instance of SarvModule or str not { sarv_module .__class__ .__name__ } ' )
91-
88+
9289 get_parms = {
9390 'method' : sarv_get_method ,
9491 'module' : module_name ,
@@ -100,46 +97,15 @@ def create_get_params(
10097
10198 return get_parms
10299
103- @staticmethod
104- def iso_time_output (output_method : TimeOutput , dt : datetime | timedelta ) -> str :
105- """
106- Generate a formatted string from a datetime or timedelta object.
107-
108- These formats are compliant with the SarvCRM API time standards.
109-
110- Args:
111- output_method (TimeOutput): Determines the output format ('date', 'datetime', or 'time').
112- dt (datetime | timedelta): A datetime or timedelta object.
113-
114- Returns:
115- str: A string representing the date, datetime, or time.
116- - date: "YYYY-MM-DD"
117- - datetime: "YYYY-MM-DDTHH:MM:SS+HH:MM"
118- - time: "HH:MM:SS"
119- """
120- if isinstance (dt , timedelta ):
121- dt = datetime .now (timezone .utc ) + dt
122-
123- if output_method == 'date' :
124- return dt .date ().isoformat ()
125-
126- elif output_method == 'datetime' :
127- return dt .astimezone ().isoformat (timespec = "seconds" )
128-
129- elif output_method == 'time' :
130- return dt .time ().isoformat (timespec = "seconds" )
131-
132- else :
133- raise TypeError (f'Invalid output method: { output_method } ' )
134-
135-
136- def send_request (
100+ def _send_request (
137101 self ,
138102 request_method : RequestMethod ,
139103 endpoint : Optional [str ] = None ,
140104 head_params : Optional [dict ] = None ,
141105 get_params : Optional [dict ] = None ,
142106 post_params : Optional [dict ] = None ,
107+ caching : bool = False ,
108+ expire_after : int = 300 ,
143109 ) -> Any :
144110 """
145111 Send a request to the Sarv API and return the response data.
@@ -160,17 +126,32 @@ def send_request(
160126 get_params = get_params or {}
161127 post_params = post_params or {}
162128
163- if self .token :
164- head_params ['Authorization' ] = f'Bearer { self .token } '
129+ if self ._token :
130+ head_params ['Authorization' ] = f'Bearer { self ._token } '
165131
166- response : requests .Response = self ._session .request (
167- method = request_method ,
168- url = self .url + f'{ endpoint if endpoint else '' } ' ,
169- headers = head_params ,
170- params = get_params ,
171- json = post_params ,
172- verify = True ,
173- )
132+ kwargs = {
133+ 'method' : request_method ,
134+ 'url' : urllib .parse .urljoin (self ._url .rstrip ('/' ) + '/' , (endpoint or '' ).lstrip ('/' )),
135+ 'headers' : head_params ,
136+ 'params' : get_params ,
137+ 'json' : post_params ,
138+ 'verify' : True ,
139+ }
140+
141+ if isinstance (self ._session , requests_cache .CachedSession ):
142+ ## Use cache
143+ if caching :
144+ kwargs .update ({'expire_after' : expire_after })
145+ response : requests .Response = self ._session .request (** kwargs )
146+
147+ ## If caching enabled but user dont want to use it
148+ else :
149+ with self ._session .cache_disabled ():
150+ response : requests .Response = self ._session .request (** kwargs )
151+
152+ else :
153+ ## Normal request without caching
154+ response : requests .Response = self ._session .request (** kwargs )
174155
175156 # Check for Server respond
176157 try :
@@ -195,6 +176,26 @@ def send_request(
195176 return response_dict .get ('data' , {})
196177
197178
179+ def enable_caching (self ) -> None :
180+ """
181+ Enables the caching and replaces `Session` with `CachedSession` or creates it.
182+ """
183+ self ._session = requests_cache .CachedSession (
184+ cache_name = 'sarv_api_cache' ,
185+ backend = self ._cache_backend ,
186+ allowable_methods = ('GET' , 'POST' ),
187+ allowable_codes = (200 ,),
188+ )
189+ self ._add_headers ()
190+
191+ def disable_caching (self ) -> None :
192+ """
193+ Disables the caching and replaces `CachedSession` with `Session` or creates it.
194+ """
195+ self ._session = requests .Session ()
196+ self ._add_headers ()
197+
198+
198199 def login (self ) -> str :
199200 """
200201 Authenticate the user and retrieve an access token.
@@ -203,25 +204,25 @@ def login(self) -> str:
203204 str: The access token for authenticated requests.
204205 """
205206 post_params = {
206- 'utype' : self .utype ,
207- 'user_name' : self .username ,
208- 'password' : self .password ,
209- 'login_type' : self .login_type ,
210- 'language' : self .language ,
207+ 'utype' : self ._utype ,
208+ 'user_name' : self ._username ,
209+ 'password' : self ._password ,
210+ 'login_type' : self ._login_type ,
211+ 'language' : self ._language ,
211212 }
212213 post_params = {k : v for k , v in post_params .items () if v is not None }
213214
214- data : Dict [str , Any ] = self .send_request (
215+ data : Dict [str , Any ] = self ._send_request (
215216 request_method = 'POST' ,
216- get_params = self .create_get_params ('Login' ),
217+ get_params = self ._create_get_params ('Login' ),
217218 post_params = post_params ,
218219 )
219220
220221 token = data .get ('token' , '' )
221222
222223 if token is not None :
223- self .token = token
224- return self .token
224+ self ._token = token
225+ return self ._token
225226 else :
226227 raise SarvException ('client did not get token from login request' )
227228
@@ -231,8 +232,8 @@ def logout(self) -> None:
231232
232233 This method should be called to invalidate the session.
233234 """
234- if self .token :
235- self .token = ''
235+ if self ._token :
236+ self ._token = ''
236237
237238
238239 def search_by_number (
@@ -250,19 +251,64 @@ def search_by_number(
250251 Returns:
251252 dict: The data related to the phone number if found.
252253 """
253- return self .send_request (
254+ return self ._send_request (
254255 request_method = 'GET' ,
255- get_params = self .create_get_params (
256+ get_params = self ._create_get_params (
256257 'SearchByNumber' ,
257258 sarv_module = module ,
258259 number = number ,
259260 ),
260261 )
261262
263+ @staticmethod
264+ def iso_time_output (output_method : TimeOutput , dt : datetime | timedelta ) -> str :
265+ """
266+ Generate a formatted string from a datetime or timedelta object.
267+
268+ These formats are compliant with the SarvCRM API time standards.
269+
270+ Args:
271+ output_method (TimeOutput): Determines the output format ('date', 'datetime', or 'time').
272+ dt (datetime | timedelta): A datetime or timedelta object.
273+
274+ Returns:
275+ str: A string representing the date, datetime, or time.
276+ - date: "YYYY-MM-DD"
277+ - datetime: "YYYY-MM-DDTHH:MM:SS+HH:MM"
278+ - time: "HH:MM:SS"
279+ """
280+ if isinstance (dt , timedelta ):
281+ dt = datetime .now (timezone .utc ) + dt
282+
283+ if output_method == 'date' :
284+ return dt .date ().isoformat ()
285+
286+ elif output_method == 'datetime' :
287+ return dt .astimezone ().isoformat (timespec = "seconds" )
288+
289+ elif output_method == 'time' :
290+ return dt .time ().isoformat (timespec = "seconds" )
291+
292+ else :
293+ raise TypeError (f'Invalid output method: { output_method } ' )
294+
295+ @staticmethod
296+ def hash_password (password : str ) -> str :
297+ """
298+ Returns the acceptable hash for SarvCRM Login
299+
300+ Args:
301+ password(str): your password
302+
303+ Returns:
304+ str: md5 hashed password
305+ """
306+ return hashlib .md5 (password .encode ('utf-8' )).hexdigest ()
307+
262308
263309 def __enter__ (self ) -> Self :
264310 """Basic Context Manager for clean code execution"""
265- if not self .token :
311+ if not self ._token :
266312 self .login ()
267313
268314 return self
@@ -279,7 +325,7 @@ def __repr__(self):
279325 Returns:
280326 str: A string containing the class name and key attributes.
281327 """
282- return f'{ self .__class__ .__name__ } (utype={ self .utype } , username={ self .username } )'
328+ return f'{ self .__class__ .__name__ } (utype={ self ._utype } , username={ self ._username } )'
283329
284330 def __str__ (self ) -> str :
285331 """
@@ -288,4 +334,4 @@ def __str__(self) -> str:
288334 Returns:
289335 str: A simplified string representation of the instance.
290336 """
291- return f'<SarvClient { self .utype } -{ self .username } >'
337+ return f'<SarvClient { self ._utype } -{ self ._username } >'
0 commit comments