panther 3.9.0__py3-none-any.whl → 4.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. panther/__init__.py +1 -1
  2. panther/_load_configs.py +168 -171
  3. panther/_utils.py +26 -49
  4. panther/app.py +85 -105
  5. panther/authentications.py +86 -55
  6. panther/background_tasks.py +25 -14
  7. panther/base_request.py +38 -14
  8. panther/base_websocket.py +172 -94
  9. panther/caching.py +60 -25
  10. panther/cli/create_command.py +20 -10
  11. panther/cli/monitor_command.py +63 -37
  12. panther/cli/template.py +38 -20
  13. panther/cli/utils.py +32 -18
  14. panther/configs.py +65 -58
  15. panther/db/connections.py +139 -0
  16. panther/db/cursor.py +43 -0
  17. panther/db/models.py +64 -29
  18. panther/db/queries/__init__.py +1 -1
  19. panther/db/queries/base_queries.py +127 -0
  20. panther/db/queries/mongodb_queries.py +77 -38
  21. panther/db/queries/pantherdb_queries.py +59 -30
  22. panther/db/queries/queries.py +232 -117
  23. panther/db/utils.py +17 -18
  24. panther/events.py +44 -0
  25. panther/exceptions.py +26 -12
  26. panther/file_handler.py +2 -2
  27. panther/generics.py +163 -0
  28. panther/logging.py +7 -2
  29. panther/main.py +111 -188
  30. panther/middlewares/base.py +3 -0
  31. panther/monitoring.py +8 -5
  32. panther/pagination.py +48 -0
  33. panther/panel/apis.py +32 -5
  34. panther/panel/urls.py +2 -1
  35. panther/permissions.py +3 -3
  36. panther/request.py +6 -13
  37. panther/response.py +114 -34
  38. panther/routings.py +83 -66
  39. panther/serializer.py +131 -25
  40. panther/test.py +31 -21
  41. panther/utils.py +28 -16
  42. panther/websocket.py +7 -4
  43. {panther-3.9.0.dist-info → panther-4.0.0.dist-info}/METADATA +93 -71
  44. panther-4.0.0.dist-info/RECORD +57 -0
  45. {panther-3.9.0.dist-info → panther-4.0.0.dist-info}/WHEEL +1 -1
  46. panther/db/connection.py +0 -92
  47. panther/middlewares/db.py +0 -18
  48. panther/middlewares/redis.py +0 -47
  49. panther-3.9.0.dist-info/RECORD +0 -54
  50. {panther-3.9.0.dist-info → panther-4.0.0.dist-info}/LICENSE +0 -0
  51. {panther-3.9.0.dist-info → panther-4.0.0.dist-info}/entry_points.txt +0 -0
  52. {panther-3.9.0.dist-info → panther-4.0.0.dist-info}/top_level.txt +0 -0
panther/base_request.py CHANGED
@@ -1,6 +1,9 @@
1
1
  from collections import namedtuple
2
2
  from collections.abc import Callable
3
3
 
4
+ from panther.db import Model
5
+ from panther.exceptions import InvalidPathVariableAPIError
6
+
4
7
 
5
8
  class Headers:
6
9
  accept: str
@@ -31,14 +34,12 @@ class Headers:
31
34
  def __getattr__(self, item: str):
32
35
  if result := self.__pythonic_headers.get(item):
33
36
  return result
34
- else:
35
- return self.__headers.get(item)
37
+ return self.__headers.get(item)
36
38
 
37
39
  def __getitem__(self, item: str):
38
40
  if result := self.__headers.get(item):
39
41
  return result
40
- else:
41
- return self.__pythonic_headers.get(item)
42
+ return self.__pythonic_headers.get(item)
42
43
 
43
44
  def __str__(self):
44
45
  items = ', '.join(f'{k}={v}' for k, v in self.__headers.items())
@@ -51,7 +52,7 @@ class Headers:
51
52
  return self.__headers
52
53
 
53
54
 
54
- Address = namedtuple('Client', ['ip', 'port'])
55
+ Address = namedtuple('Address', ['ip', 'port'])
55
56
 
56
57
 
57
58
  class BaseRequest:
@@ -59,11 +60,10 @@ class BaseRequest:
59
60
  self.scope = scope
60
61
  self.asgi_send = send
61
62
  self.asgi_receive = receive
62
- self._data = ...
63
- self._validated_data = None
64
- self._user = None
65
63
  self._headers: Headers | None = None
66
64
  self._params: dict | None = None
65
+ self.user: Model | None = None
66
+ self.path_variables: dict | None = None
67
67
 
68
68
  @property
69
69
  def headers(self) -> Headers:
@@ -103,9 +103,33 @@ class BaseRequest:
103
103
  def scheme(self) -> str:
104
104
  return self.scope['scheme']
105
105
 
106
- @property
107
- def user(self):
108
- return self._user
109
-
110
- def set_user(self, user) -> None:
111
- self._user = user
106
+ def collect_path_variables(self, found_path: str):
107
+ self.path_variables = {
108
+ variable.strip('< >'): value
109
+ for variable, value in zip(
110
+ found_path.strip('/').split('/'),
111
+ self.path.strip('/').split('/')
112
+ )
113
+ if variable.startswith('<')
114
+ }
115
+
116
+ def clean_parameters(self, func: Callable) -> dict:
117
+ kwargs = self.path_variables.copy()
118
+
119
+ for variable_name, variable_type in func.__annotations__.items():
120
+ # Put Request/ Websocket In kwargs (If User Wants It)
121
+ if issubclass(variable_type, BaseRequest):
122
+ kwargs[variable_name] = self
123
+
124
+ elif variable_name in kwargs:
125
+ # Cast To Boolean
126
+ if variable_type is bool:
127
+ kwargs[variable_name] = kwargs[variable_name].lower() not in ['false', '0']
128
+
129
+ # Cast To Int
130
+ elif variable_type is int:
131
+ try:
132
+ kwargs[variable_name] = int(kwargs[variable_name])
133
+ except ValueError:
134
+ raise InvalidPathVariableAPIError(value=kwargs[variable_name], variable_type=variable_type)
135
+ return kwargs
panther/base_websocket.py CHANGED
@@ -1,28 +1,28 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
- import contextlib
5
4
  import logging
6
- from multiprocessing import Manager
5
+ from multiprocessing.managers import SyncManager
7
6
  from typing import TYPE_CHECKING, Literal
8
7
 
9
8
  import orjson as json
10
9
 
11
10
  from panther import status
12
- from panther._utils import generate_ws_connection_id
13
11
  from panther.base_request import BaseRequest
14
12
  from panther.configs import config
15
- from panther.db.connection import redis
16
- from panther.utils import Singleton
13
+ from panther.db.connections import redis
14
+ from panther.exceptions import AuthenticationAPIError, InvalidPathVariableAPIError
15
+ from panther.monitoring import Monitoring
16
+ from panther.utils import Singleton, ULID
17
17
 
18
18
  if TYPE_CHECKING:
19
- from redis import Redis
19
+ from redis.asyncio import Redis
20
20
 
21
21
  logger = logging.getLogger('panther')
22
22
 
23
23
 
24
24
  class PubSub:
25
- def __init__(self, manager):
25
+ def __init__(self, manager: SyncManager):
26
26
  self._manager = manager
27
27
  self._subscribers = self._manager.list()
28
28
 
@@ -37,17 +37,29 @@ class PubSub:
37
37
 
38
38
 
39
39
  class WebsocketConnections(Singleton):
40
- def __init__(self, manager: Manager = None):
40
+ def __init__(self, pubsub_connection: Redis | SyncManager):
41
41
  self.connections = {}
42
42
  self.connections_count = 0
43
- self.manager = manager
43
+ self.pubsub_connection = pubsub_connection
44
44
 
45
- def __call__(self, r: Redis | None):
46
- if r:
47
- subscriber = r.pubsub()
48
- subscriber.subscribe('websocket_connections')
45
+ if isinstance(self.pubsub_connection, SyncManager):
46
+ self.pubsub = PubSub(manager=self.pubsub_connection)
47
+
48
+ async def __call__(self):
49
+ if isinstance(self.pubsub_connection, SyncManager):
50
+ # We don't have redis connection, so use the `multiprocessing.Manager`
51
+ self.pubsub: PubSub
52
+ queue = self.pubsub.subscribe()
53
+ logger.info("Subscribed to 'websocket_connections' queue")
54
+ while True:
55
+ received_message = queue.get()
56
+ await self._handle_received_message(received_message=received_message)
57
+ else:
58
+ # We have a redis connection, so use it for pubsub
59
+ self.pubsub = self.pubsub_connection.pubsub()
60
+ await self.pubsub.subscribe('websocket_connections')
49
61
  logger.info("Subscribed to 'websocket_connections' channel")
50
- for channel_data in subscriber.listen():
62
+ async for channel_data in self.pubsub.listen():
51
63
  match channel_data['type']:
52
64
  # Subscribed
53
65
  case 'subscribe':
@@ -56,19 +68,12 @@ class WebsocketConnections(Singleton):
56
68
  # Message Received
57
69
  case 'message':
58
70
  loaded_data = json.loads(channel_data['data'].decode())
59
- self._handle_received_message(received_message=loaded_data)
71
+ await self._handle_received_message(received_message=loaded_data)
60
72
 
61
73
  case unknown_type:
62
- logger.debug(f'Unknown Channel Type: {unknown_type}')
63
- else:
64
- self.pubsub = PubSub(manager=self.manager)
65
- queue = self.pubsub.subscribe()
66
- logger.info("Subscribed to 'websocket_connections' queue")
67
- while True:
68
- received_message = queue.get()
69
- self._handle_received_message(received_message=received_message)
74
+ logger.error(f'Unknown Channel Type: {unknown_type}')
70
75
 
71
- def _handle_received_message(self, received_message):
76
+ async def _handle_received_message(self, received_message):
72
77
  if (
73
78
  isinstance(received_message, dict)
74
79
  and (connection_id := received_message.get('connection_id'))
@@ -79,71 +84,156 @@ class WebsocketConnections(Singleton):
79
84
  # Check Action of WS
80
85
  match received_message['action']:
81
86
  case 'send':
82
- asyncio.run(self.connections[connection_id].send(data=received_message['data']))
87
+ await self.connections[connection_id].send(data=received_message['data'])
83
88
  case 'close':
84
- with contextlib.suppress(RuntimeError):
85
- asyncio.run(self.connections[connection_id].close(
86
- code=received_message['data']['code'],
87
- reason=received_message['data']['reason']
88
- ))
89
- # We are trying to disconnect the connection between a thread and a user
90
- # from another thread, it's working, but we have to find another solution for it
91
- #
92
- # Error:
93
- # Task <Task pending coro=<Websocket.close()>> got Future
94
- # <Task pending coro=<WebSocketCommonProtocol.transfer_data()>>
95
- # attached to a different loop
89
+ await self.connections[connection_id].close(
90
+ code=received_message['data']['code'],
91
+ reason=received_message['data']['reason']
92
+ )
96
93
  case unknown_action:
97
- logger.debug(f'Unknown Message Action: {unknown_action}')
94
+ logger.error(f'Unknown Message Action: {unknown_action}')
98
95
 
99
- def publish(self, connection_id: str, action: Literal['send', 'close'], data: any):
96
+ async def publish(self, connection_id: str, action: Literal['send', 'close'], data: any):
100
97
  publish_data = {'connection_id': connection_id, 'action': action, 'data': data}
101
98
 
102
99
  if redis.is_connected:
103
- redis.publish('websocket_connections', json.dumps(publish_data))
100
+ await redis.publish('websocket_connections', json.dumps(publish_data))
104
101
  else:
105
102
  self.pubsub.publish(publish_data)
106
103
 
107
- async def new_connection(self, connection: Websocket) -> None:
108
- await connection.connect(**connection.path_variables)
109
- if not hasattr(connection, '_connection_id'):
110
- # User didn't even call the `self.accept()` so close the connection
111
- await connection.close()
104
+ async def listen(self, connection: Websocket) -> None:
105
+ # 1. Authentication
106
+ if not connection.is_rejected:
107
+ await self.handle_authentication(connection=connection)
112
108
 
113
- if connection.is_connected:
114
- self.connections_count += 1
109
+ # 2. Permissions
110
+ if not connection.is_rejected:
111
+ await self.handle_permissions(connection=connection)
115
112
 
116
- # Save New ConnectionID
117
- self.connections[connection.connection_id] = connection
113
+ if connection.is_rejected:
114
+ # Connection is rejected so don't continue the flow ...
115
+ return None
118
116
 
119
- def remove_connection(self, connection: Websocket) -> None:
120
- if connection.is_connected:
121
- self.connections_count -= 1
122
- del self.connections[connection.connection_id]
117
+ # 3. Put PathVariables and Request(If User Wants It) In kwargs
118
+ try:
119
+ kwargs = connection.clean_parameters(connection.connect)
120
+ except InvalidPathVariableAPIError as e:
121
+ connection.log(e.detail)
122
+ return await connection.close()
123
123
 
124
+ # 4. Connect To Endpoint
125
+ await connection.connect(**kwargs)
124
126
 
125
- class Websocket(BaseRequest):
126
- is_connected: bool = False
127
+ # 5. Check Connection
128
+ if not connection.is_connected and not connection.is_rejected:
129
+ # User didn't call the `self.accept()` or `self.close()` so we `close()` the connection (reject)
130
+ return await connection.close()
127
131
 
128
- async def connect(self, **kwargs) -> None:
129
- """Check your conditions then self.accept() the connection"""
130
- await self.accept()
132
+ # 6. Listen Connection
133
+ await self.listen_connection(connection=connection)
131
134
 
132
- async def accept(self, subprotocol: str | None = None, headers: dict | None = None) -> None:
133
- await self.asgi_send({'type': 'websocket.accept', 'subprotocol': subprotocol, 'headers': headers or {}})
134
- self.is_connected = True
135
+ async def listen_connection(self, connection: Websocket):
136
+ while True:
137
+ response = await connection.asgi_receive()
138
+ if response['type'] == 'websocket.connect':
139
+ continue
140
+
141
+ if response['type'] == 'websocket.disconnect':
142
+ # Connect has to be closed by the client
143
+ await self.connection_closed(connection=connection)
144
+ break
135
145
 
146
+ if 'text' in response:
147
+ await connection.receive(data=response['text'])
148
+ else:
149
+ await connection.receive(data=response['bytes'])
150
+
151
+ async def connection_accepted(self, connection: Websocket) -> None:
136
152
  # Generate ConnectionID
137
- connection_id = generate_ws_connection_id()
138
- while connection_id in config['websocket_connections'].connections:
139
- connection_id = generate_ws_connection_id()
153
+ connection._connection_id = ULID.new()
154
+
155
+ # Save Connection
156
+ self.connections[connection.connection_id] = connection
140
157
 
141
- # Set ConnectionID
142
- self.set_connection_id(connection_id)
158
+ # Logs
159
+ await connection.monitoring.after('Accepted')
160
+ connection.log(f'Accepted {connection.connection_id}')
161
+
162
+ async def connection_closed(self, connection: Websocket, from_server: bool = False) -> None:
163
+ if connection.is_connected:
164
+ del self.connections[connection.connection_id]
165
+ await connection.monitoring.after('Closed')
166
+ connection.log(f'Closed {connection.connection_id}')
167
+ connection._connection_id = ''
168
+
169
+ elif connection.is_rejected is False and from_server is True:
170
+ await connection.monitoring.after('Rejected')
171
+ connection.log('Rejected')
172
+ connection._is_rejected = True
173
+
174
+ async def start(self):
175
+ """
176
+ Start Websocket Listener (Redis/ Queue)
177
+
178
+ Cause of --preload in gunicorn we have to keep this function here,
179
+ and we can't move it to __init__ of Panther
180
+
181
+ * Each process should start this listener for itself,
182
+ but they have same Manager()
183
+ """
184
+
185
+ if config.HAS_WS:
186
+ # Schedule the async function to run in the background,
187
+ # We don't need to await for this task
188
+ asyncio.create_task(self())
189
+
190
+ @classmethod
191
+ async def handle_authentication(cls, connection: Websocket):
192
+ """Return True if connection is closed, False otherwise."""
193
+ if connection.auth:
194
+ if not config.WS_AUTHENTICATION:
195
+ logger.critical('`WS_AUTHENTICATION` has not been set in configs')
196
+ await connection.close()
197
+ else:
198
+ try:
199
+ connection.user = await config.WS_AUTHENTICATION.authentication(connection)
200
+ except AuthenticationAPIError as e:
201
+ connection.log(e.detail)
202
+ await connection.close()
203
+
204
+ @classmethod
205
+ async def handle_permissions(cls, connection: Websocket):
206
+ """Return True if connection is closed, False otherwise."""
207
+ for perm in connection.permissions:
208
+ if type(perm.authorization).__name__ != 'method':
209
+ logger.critical(f'{perm.__name__}.authorization should be "classmethod"')
210
+ await connection.close()
211
+ elif await perm.authorization(connection) is False:
212
+ connection.log('Permission Denied')
213
+ await connection.close()
214
+
215
+
216
+ class Websocket(BaseRequest):
217
+ auth: bool = False
218
+ permissions: list = []
219
+ _connection_id: str = ''
220
+ _is_rejected: bool = False
221
+ _monitoring: Monitoring
222
+
223
+ def __init_subclass__(cls, **kwargs):
224
+ if cls.__module__ != 'panther.websocket':
225
+ config.HAS_WS = True
226
+
227
+ async def connect(self, **kwargs) -> None:
228
+ pass
143
229
 
144
230
  async def receive(self, data: str | bytes) -> None:
145
231
  pass
146
232
 
233
+ async def accept(self, subprotocol: str | None = None, headers: dict | None = None) -> None:
234
+ await self.asgi_send({'type': 'websocket.accept', 'subprotocol': subprotocol, 'headers': headers or {}})
235
+ await config.WEBSOCKET_CONNECTIONS.connection_accepted(connection=self)
236
+
147
237
  async def send(self, data: any = None) -> None:
148
238
  logger.debug(f'Sending WS Message to {self.connection_id}')
149
239
  if data:
@@ -161,38 +251,26 @@ class Websocket(BaseRequest):
161
251
  await self.asgi_send({'type': 'websocket.send', 'bytes': bytes_data})
162
252
 
163
253
  async def close(self, code: int = status.WS_1000_NORMAL_CLOSURE, reason: str = '') -> None:
164
- logger.debug(f'Closing WS Connection {self.connection_id}')
165
- self.is_connected = False
166
- config['websocket_connections'].remove_connection(self)
167
254
  await self.asgi_send({'type': 'websocket.close', 'code': code, 'reason': reason})
255
+ await config.WEBSOCKET_CONNECTIONS.connection_closed(connection=self, from_server=True)
168
256
 
169
- async def listen(self) -> None:
170
- while self.is_connected:
171
- response = await self.asgi_receive()
172
- if response['type'] == 'websocket.connect':
173
- continue
174
-
175
- if response['type'] == 'websocket.disconnect':
176
- break
177
-
178
- if 'text' in response:
179
- await self.receive(data=response['text'])
180
- else:
181
- await self.receive(data=response['bytes'])
182
-
183
- def set_path_variables(self, path_variables: dict) -> None:
184
- self._path_variables = path_variables
257
+ @property
258
+ def connection_id(self) -> str:
259
+ if self.is_connected:
260
+ return self._connection_id
261
+ logger.error('You should first `self.accept()` the connection then use the `self.connection_id`')
185
262
 
186
263
  @property
187
- def path_variables(self) -> dict:
188
- return getattr(self, '_path_variables', {})
264
+ def is_connected(self) -> bool:
265
+ return bool(self._connection_id)
189
266
 
190
- def set_connection_id(self, connection_id: str) -> None:
191
- self._connection_id = connection_id
267
+ @property
268
+ def is_rejected(self) -> bool:
269
+ return self._is_rejected
192
270
 
193
271
  @property
194
- def connection_id(self) -> str:
195
- connection_id = getattr(self, '_connection_id', None)
196
- if connection_id is None:
197
- logger.error('You should first `self.accept()` the connection then use the `self.connection_id`')
198
- return connection_id
272
+ def monitoring(self) -> Monitoring:
273
+ return self._monitoring
274
+
275
+ def log(self, message: str):
276
+ logger.debug(f'WS {self.path} --> {message}')
panther/caching.py CHANGED
@@ -6,54 +6,56 @@ from types import NoneType
6
6
  import orjson as json
7
7
 
8
8
  from panther.configs import config
9
- from panther.db.connection import redis
9
+ from panther.db.connections import redis
10
10
  from panther.request import Request
11
11
  from panther.response import Response, ResponseDataTypes
12
+ from panther.throttling import throttling_storage
12
13
  from panther.utils import generate_hash_value_from_string, round_datetime
13
14
 
14
15
  logger = logging.getLogger('panther')
15
16
 
16
17
  caches = {}
17
- CachedResponse = namedtuple('Cached', ['data', 'status_code'])
18
+ CachedResponse = namedtuple('CachedResponse', ['data', 'status_code'])
18
19
 
19
20
 
20
- def cache_key(request: Request, /) -> str:
21
+ def api_cache_key(request: Request, cache_exp_time: timedelta | None = None) -> str:
21
22
  client = request.user and request.user.id or request.client.ip
22
23
  query_params_hash = generate_hash_value_from_string(request.scope['query_string'].decode('utf-8'))
23
- return f'{client}-{request.path}-{query_params_hash}-{request.validated_data}'
24
+ key = f'{client}-{request.path}-{query_params_hash}-{request.validated_data}'
24
25
 
25
-
26
- def local_cache_key(*, request: Request, cache_exp_time: timedelta | None = None) -> str:
27
- key = cache_key(request)
28
26
  if cache_exp_time:
29
27
  time = round_datetime(datetime.now(), cache_exp_time)
30
28
  return f'{time}-{key}'
31
- else:
32
- return key
33
29
 
30
+ return key
34
31
 
35
- def get_cached_response_data(*, request: Request, cache_exp_time: timedelta) -> CachedResponse | None:
32
+
33
+ def throttling_cache_key(request: Request, duration: timedelta) -> str:
34
+ client = request.user and request.user.id or request.client.ip
35
+ time = round_datetime(datetime.now(), duration)
36
+ return f'{time}-{client}-{request.path}'
37
+
38
+
39
+ async def get_response_from_cache(*, request: Request, cache_exp_time: timedelta) -> CachedResponse | None:
36
40
  """
37
41
  If redis.is_connected:
38
42
  Get Cached Data From Redis
39
43
  else:
40
44
  Get Cached Data From Memory
41
45
  """
42
- if redis.is_connected: # noqa: Unresolved References
43
- key = cache_key(request)
44
- data = (redis.get(key) or b'{}').decode()
46
+ if redis.is_connected:
47
+ key = api_cache_key(request=request)
48
+ data = (await redis.get(key) or b'{}').decode()
45
49
  if cached_value := json.loads(data):
46
50
  return CachedResponse(*cached_value)
47
51
 
48
52
  else:
49
- key = local_cache_key(request=request, cache_exp_time=cache_exp_time)
53
+ key = api_cache_key(request=request, cache_exp_time=cache_exp_time)
50
54
  if cached_value := caches.get(key):
51
55
  return CachedResponse(*cached_value)
52
56
 
53
- return None
54
-
55
57
 
56
- def set_cache_response(*, request: Request, response: Response, cache_exp_time: timedelta | int) -> None:
58
+ async def set_response_in_cache(*, request: Request, response: Response, cache_exp_time: timedelta | int) -> None:
57
59
  """
58
60
  If redis.is_connected:
59
61
  Cache The Data In Redis
@@ -63,10 +65,10 @@ def set_cache_response(*, request: Request, response: Response, cache_exp_time:
63
65
 
64
66
  cache_data: tuple[ResponseDataTypes, int] = (response.data, response.status_code)
65
67
 
66
- if redis.is_connected: # noqa: Unresolved References
67
- key = cache_key(request)
68
+ if redis.is_connected:
69
+ key = api_cache_key(request=request)
68
70
 
69
- cache_exp_time = cache_exp_time or config['default_cache_exp']
71
+ cache_exp_time = cache_exp_time or config.DEFAULT_CACHE_EXP
70
72
  cache_data: bytes = json.dumps(cache_data)
71
73
 
72
74
  if not isinstance(cache_exp_time, timedelta | int | NoneType):
@@ -76,15 +78,48 @@ def set_cache_response(*, request: Request, response: Response, cache_exp_time:
76
78
  if cache_exp_time is None:
77
79
  logger.warning(
78
80
  'your response are going to cache in redis forever '
79
- '** set DEFAULT_CACHE_EXP in configs or pass the cache_exp_time in @API.get() for prevent this **'
81
+ '** set DEFAULT_CACHE_EXP in `configs` or set the `cache_exp_time` in `@API.get()` to prevent this **'
80
82
  )
81
- redis.set(key, cache_data)
83
+ await redis.set(key, cache_data)
82
84
  else:
83
- redis.set(key, cache_data, ex=cache_exp_time)
85
+ await redis.set(key, cache_data, ex=cache_exp_time)
84
86
 
85
87
  else:
86
- key = local_cache_key(request=request, cache_exp_time=cache_exp_time)
88
+ key = api_cache_key(request=request, cache_exp_time=cache_exp_time)
87
89
  caches[key] = cache_data
88
90
 
89
91
  if cache_exp_time:
90
- logger.info('"cache_exp_time" is not very accurate when redis is not connected.')
92
+ logger.info('`cache_exp_time` is not very accurate when `redis` is not connected.')
93
+
94
+
95
+ async def get_throttling_from_cache(request: Request, duration: timedelta) -> int:
96
+ """
97
+ If redis.is_connected:
98
+ Get Cached Data From Redis
99
+ else:
100
+ Get Cached Data From Memory
101
+ """
102
+ key = throttling_cache_key(request=request, duration=duration)
103
+
104
+ if redis.is_connected:
105
+ data = (await redis.get(key) or b'0').decode()
106
+ return json.loads(data)
107
+
108
+ else:
109
+ return throttling_storage[key]
110
+
111
+
112
+ async def increment_throttling_in_cache(request: Request, duration: timedelta) -> None:
113
+ """
114
+ If redis.is_connected:
115
+ Increment The Data In Redis
116
+ else:
117
+ Increment The Data In Memory
118
+ """
119
+ key = throttling_cache_key(request=request, duration=duration)
120
+
121
+ if redis.is_connected:
122
+ await redis.incrby(key, amount=1)
123
+
124
+ else:
125
+ throttling_storage[key] += 1