Skip to content

Commit 472ca64

Browse files
committed
Version 1.1.2 Added Caching
1 parent a1cdcb3 commit 472ca64

12 files changed

Lines changed: 220 additions & 143 deletions

File tree

pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "py-sarvcrm-api"
7-
version = "1.1.1"
7+
version = "1.1.2"
88
description = "Simple sarvcrm api module"
99
readme = "README.md"
1010
license = { file = "LICENSE" }
@@ -18,7 +18,8 @@ classifiers = [
1818
"Operating System :: OS Independent",
1919
]
2020
dependencies = [
21-
"requests",
21+
"requests==2.32.4",
22+
"requests_cache==1.2.1",
2223
]
2324
requires-python = ">=3.9"
2425

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
requests
2+
requests_cache
23
setuptools
34

45
python-dotenv

sarvcrm_api/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from .modules._base import SarvModule
55
from .__version__ import __version__ as version
66

7+
78
__all__ = [
89
'SarvClient',
910
'SarvURL',

sarvcrm_api/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = '1.1.1'
1+
__version__ = '1.1.2'
22
__letter__ = 'v'
33

44
def get_version():

sarvcrm_api/_client.py

Lines changed: 132 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
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
34
from datetime import datetime, timedelta, timezone
45
from ._exceptions import SarvException
56
from ._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}>'

sarvcrm_api/_mixins.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,4 @@ def __init__(self) -> None:
4242
self.ServiceCenters = ServiceCenters(self)
4343
self.Tasks = Tasks(self)
4444
self.Timesheet = Timesheet(self)
45-
self.Vendors = Vendors(self)
45+
self.Vendors = Vendors(self)

sarvcrm_api/_type_hints.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
'SearchByNumber',
1414
]
1515

16+
1617
__all__ = [
1718
'TimeOutput',
1819
'RequestMethod',

sarvcrm_api/_url.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
SarvURL = SarvAPI_v5
77

8+
89
__all__ = [
910
'SarvAPI_v5',
1011
'SarvFrontend',

0 commit comments

Comments
 (0)