panther 5.0.0b3__py3-none-any.whl → 5.0.0b5__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 (57) hide show
  1. panther/__init__.py +1 -1
  2. panther/_load_configs.py +46 -37
  3. panther/_utils.py +49 -34
  4. panther/app.py +96 -97
  5. panther/authentications.py +97 -50
  6. panther/background_tasks.py +98 -124
  7. panther/base_request.py +16 -10
  8. panther/base_websocket.py +8 -8
  9. panther/caching.py +16 -80
  10. panther/cli/create_command.py +17 -16
  11. panther/cli/main.py +1 -1
  12. panther/cli/monitor_command.py +11 -6
  13. panther/cli/run_command.py +5 -71
  14. panther/cli/template.py +7 -7
  15. panther/cli/utils.py +58 -69
  16. panther/configs.py +70 -72
  17. panther/db/connections.py +30 -24
  18. panther/db/cursor.py +3 -1
  19. panther/db/models.py +26 -10
  20. panther/db/queries/base_queries.py +4 -5
  21. panther/db/queries/mongodb_queries.py +21 -21
  22. panther/db/queries/pantherdb_queries.py +1 -1
  23. panther/db/queries/queries.py +26 -8
  24. panther/db/utils.py +1 -1
  25. panther/events.py +25 -14
  26. panther/exceptions.py +2 -7
  27. panther/file_handler.py +1 -1
  28. panther/generics.py +74 -100
  29. panther/logging.py +2 -1
  30. panther/main.py +12 -13
  31. panther/middlewares/cors.py +67 -0
  32. panther/middlewares/monitoring.py +5 -3
  33. panther/openapi/urls.py +2 -2
  34. panther/openapi/utils.py +3 -3
  35. panther/openapi/views.py +20 -37
  36. panther/pagination.py +4 -2
  37. panther/panel/apis.py +2 -7
  38. panther/panel/urls.py +2 -6
  39. panther/panel/utils.py +9 -5
  40. panther/panel/views.py +13 -22
  41. panther/permissions.py +2 -1
  42. panther/request.py +2 -1
  43. panther/response.py +101 -94
  44. panther/routings.py +12 -12
  45. panther/serializer.py +20 -43
  46. panther/test.py +73 -58
  47. panther/throttling.py +68 -3
  48. panther/utils.py +5 -11
  49. panther-5.0.0b5.dist-info/METADATA +188 -0
  50. panther-5.0.0b5.dist-info/RECORD +75 -0
  51. panther/monitoring.py +0 -34
  52. panther-5.0.0b3.dist-info/METADATA +0 -223
  53. panther-5.0.0b3.dist-info/RECORD +0 -75
  54. {panther-5.0.0b3.dist-info → panther-5.0.0b5.dist-info}/WHEEL +0 -0
  55. {panther-5.0.0b3.dist-info → panther-5.0.0b5.dist-info}/entry_points.txt +0 -0
  56. {panther-5.0.0b3.dist-info → panther-5.0.0b5.dist-info}/licenses/LICENSE +0 -0
  57. {panther-5.0.0b3.dist-info → panther-5.0.0b5.dist-info}/top_level.txt +0 -0
@@ -1,23 +1,38 @@
1
+ """
2
+ Example:
3
+ -------------------------------------------------------------
4
+ >>> import datetime
5
+
6
+
7
+ >>> async def hello(name: str):
8
+ >>> print(f'Hello {name}')
9
+
10
+ # Run it every 5 seconds for 2 times
11
+ >>> BackgroundTask(hello, 'Ali').interval(2).every_seconds(5).submit()
12
+
13
+ # Run it every day at 08:00 O'clock forever
14
+ >>> BackgroundTask(hello, 'Saba').interval(-1).every_days().at(datetime.time(hour=8)).submit()
15
+ """
16
+
1
17
  import asyncio
2
18
  import datetime
3
19
  import logging
4
20
  import sys
5
21
  import time
6
- from threading import Thread
7
- from typing import Callable, Literal
22
+ from enum import Enum
23
+ from threading import Lock, Thread
24
+ from typing import TYPE_CHECKING, Any, Literal
8
25
 
9
26
  from panther._utils import is_function_async
10
- from panther.utils import Singleton
27
+ from panther.utils import Singleton, timezone_now
11
28
 
12
- __all__ = (
13
- 'BackgroundTask',
14
- 'background_tasks',
15
- )
29
+ if TYPE_CHECKING:
30
+ from collections.abc import Callable
16
31
 
32
+ __all__ = ('BackgroundTask', 'WeekDay')
17
33
 
18
34
  logger = logging.getLogger('panther')
19
35
 
20
-
21
36
  if sys.version_info.minor >= 11:
22
37
  from typing import Self
23
38
  else:
@@ -26,123 +41,102 @@ else:
26
41
  Self = TypeVar('Self', bound='BackgroundTask')
27
42
 
28
43
 
44
+ class WeekDay(Enum):
45
+ MONDAY = 0
46
+ TUESDAY = 1
47
+ WEDNESDAY = 2
48
+ THURSDAY = 3
49
+ FRIDAY = 4
50
+ SATURDAY = 5
51
+ SUNDAY = 6
52
+
53
+
29
54
  class BackgroundTask:
30
55
  """
31
- Default: Task is going to run once,
32
- and if you only specify a custom interval, default interval time is 1 minutes
56
+ Schedules and runs a function periodically in the background.
57
+
58
+ Default: Task runs once. If only a custom interval is specified, default interval time is 1 minute.
59
+ Use submit() to add the task to the background queue.
33
60
  """
34
- def __init__(self, func, *args, **kwargs):
35
- self._func: Callable = func
61
+
62
+ def __init__(self, func: 'Callable', *args: Any, **kwargs: Any):
63
+ self._func: 'Callable' = func
36
64
  self._args: tuple = args
37
65
  self._kwargs: dict = kwargs
38
66
  self._remaining_interval: int = 1
39
67
  self._last_run: datetime.datetime | None = None
40
68
  self._timedelta: datetime.timedelta = datetime.timedelta(minutes=1)
41
69
  self._time: datetime.time | None = None
42
- self._day_of_week: int | None = None
70
+ self._day_of_week: WeekDay | None = None
43
71
  self._unit: Literal['seconds', 'minutes', 'hours', 'days', 'weeks'] | None = None
44
72
 
45
73
  def interval(self, interval: int, /) -> Self:
46
- """
47
- interval = -1 --> Infinite
48
- """
74
+ """Set how many times to run the task. interval = -1 for infinite."""
49
75
  self._remaining_interval = interval
50
76
  return self
51
77
 
52
78
  def every_seconds(self, seconds: int = 1, /) -> Self:
53
- """
54
- Every How Many Seconds? (Default is 1)
55
- """
79
+ """Run every N seconds (default 1)."""
56
80
  self._unit = 'seconds'
57
81
  self._timedelta = datetime.timedelta(seconds=seconds)
58
82
  return self
59
83
 
60
84
  def every_minutes(self, minutes: int = 1, /) -> Self:
61
- """
62
- Every How Many Minutes? (Default is 1)
63
- """
85
+ """Run every N minutes (default 1)."""
64
86
  self._unit = 'minutes'
65
87
  self._timedelta = datetime.timedelta(minutes=minutes)
66
88
  return self
67
89
 
68
90
  def every_hours(self, hours: int = 1, /) -> Self:
69
- """
70
- Every How Many Hours? (Default is 1)
71
- """
91
+ """Run every N hours (default 1)."""
72
92
  self._unit = 'hours'
73
93
  self._timedelta = datetime.timedelta(hours=hours)
74
94
  return self
75
95
 
76
96
  def every_days(self, days: int = 1, /) -> Self:
77
- """
78
- Every How Many Days? (Default is 1)
79
- """
97
+ """Run every N days (default 1)."""
80
98
  self._unit = 'days'
81
99
  self._timedelta = datetime.timedelta(days=days)
82
100
  return self
83
101
 
84
102
  def every_weeks(self, weeks: int = 1, /) -> Self:
85
- """
86
- Every How Many Weeks? (Default is 1)
87
- """
103
+ """Run every N weeks (default 1)."""
88
104
  self._unit = 'weeks'
89
105
  self._timedelta = datetime.timedelta(weeks=weeks)
90
106
  return self
91
107
 
92
- def on(
93
- self,
94
- day_of_week: Literal['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'],
95
- /
96
- ) -> Self:
108
+ def on(self, day_of_week: WeekDay, /) -> Self:
97
109
  """
98
- Set day to schedule the task, useful on `.every_weeks()`
110
+ Set day to schedule the task. Accepts string like 'monday', 'tuesday', etc.
99
111
  """
100
- week_days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']
101
- if day_of_week not in week_days:
102
- msg = f'Argument should be one of {week_days}'
103
- raise TypeError(msg)
104
-
105
- self._day_of_week = week_days.index(day_of_week)
106
-
107
- if self._unit != 'weeks':
108
- logger.warning('`.on()` only useful when you are using `.every_weeks()`')
112
+ self._day_of_week = day_of_week
109
113
  return self
110
114
 
111
115
  def at(self, _time: datetime.time, /) -> Self:
112
- """
113
- Set a time to schedule the task,
114
- Only useful on `.every_days()` and `.every_weeks()`
115
- """
116
+ """Set a time to schedule the task."""
116
117
  if isinstance(_time, datetime.time):
117
118
  self._time = _time
118
119
  elif isinstance(_time, datetime.datetime):
119
- _time = _time.time()
120
+ self._time = _time.time()
120
121
  else:
121
122
  raise TypeError(
122
- f'Argument should be instance of `datetime.time()` or `datetime.datetime()` not `{type(_time)}`')
123
-
124
- if self._unit not in ['days', 'weeks']:
125
- logger.warning('`.at()` only useful when you are using `.every_days()` or `.every_weeks()`')
126
-
123
+ f'Argument should be instance of `datetime.time()` or `datetime.datetime()` not `{type(_time)}`',
124
+ )
127
125
  return self
128
126
 
129
127
  def _should_wait(self) -> bool:
130
128
  """
131
- Return:
132
- ------
133
- True: Wait and do nothing
134
- False: Don't wait and Run this task
129
+ Returns True if the task should wait (not run yet), False if it should run now.
135
130
  """
136
- now = datetime.datetime.now()
131
+ now = timezone_now()
137
132
 
138
133
  # Wait
139
134
  if self._last_run and (self._last_run + self._timedelta) > now:
140
135
  return True
141
136
 
142
137
  # Check day of week
143
- if self._day_of_week is not None:
144
- if self._day_of_week != now.weekday():
145
- return True
138
+ if self._day_of_week is not None and self._day_of_week.value != now.weekday():
139
+ return True
146
140
 
147
141
  # We don't have time condition, so run
148
142
  if self._time is None:
@@ -150,31 +144,21 @@ class BackgroundTask:
150
144
  return False
151
145
 
152
146
  # Time is ok, so run
153
- if bool(
154
- now.hour == self._time.hour and
155
- now.minute == self._time.minute and
156
- now.second == self._time.second,
157
- ):
147
+ if now.hour == self._time.hour and now.minute == self._time.minute and now.second == self._time.second:
158
148
  self._last_run = now
159
149
  return False
160
150
 
161
- # Time was not ok
151
+ # Time was not ok, wait
162
152
  return True
163
153
 
164
154
  def __call__(self) -> bool:
165
155
  """
166
- Return:
167
- ------
168
- True: Everything is ok
169
- False: This task is done, remove it from `BackgroundTasks.tasks`
156
+ Executes the task if it's time. Returns True if the task should remain scheduled, False if done.
170
157
  """
171
158
  if self._remaining_interval == 0:
172
159
  return False
173
-
174
160
  if self._should_wait():
175
- # Just wait, it's not your time yet :)
176
161
  return True
177
-
178
162
  logger.info(
179
163
  f'{self._func.__name__}('
180
164
  f'{", ".join(str(a) for a in self._args)}, '
@@ -183,51 +167,62 @@ class BackgroundTask:
183
167
  )
184
168
  if self._remaining_interval != -1:
185
169
  self._remaining_interval -= 1
186
- if is_function_async(self._func):
187
- asyncio.run(self._func(*self._args, **self._kwargs))
188
- else:
189
- self._func(*self._args, **self._kwargs)
190
-
170
+ try:
171
+ if is_function_async(self._func):
172
+ asyncio.run(self._func(*self._args, **self._kwargs))
173
+ else:
174
+ self._func(*self._args, **self._kwargs)
175
+ except Exception as e:
176
+ logger.error(f'Exception in background task {self._func.__name__}: {e}', exc_info=True)
191
177
  return True
192
178
 
179
+ def submit(self) -> Self:
180
+ """Add this task to the background task queue."""
181
+ _background_tasks.add_task(self)
182
+ return self
183
+
193
184
 
194
185
  class BackgroundTasks(Singleton):
195
186
  _initialized: bool = False
196
187
 
197
188
  def __init__(self):
198
- self.tasks = []
189
+ self.tasks: list[BackgroundTask] = []
190
+ self._lock = Lock()
199
191
 
200
192
  def add_task(self, task: BackgroundTask):
201
193
  if self._initialized is False:
202
194
  logger.error('Task will be ignored, `BACKGROUND_TASKS` is not True in `configs`')
203
195
  return
204
-
205
196
  if not self._is_instance_of_task(task):
206
197
  return
207
-
208
- if task not in self.tasks:
209
- self.tasks.append(task)
198
+ with self._lock:
199
+ if task not in self.tasks:
200
+ self.tasks.append(task)
201
+ logger.info(f'Task {task._func.__name__} submitted.')
210
202
 
211
203
  def initialize(self):
212
- """
213
- We only call initialize() once in the panther.main.Panther.load_configs()
214
- """
215
-
216
- def __run_task(task):
217
- if task() is False:
218
- self.tasks.remove(task)
219
-
220
- def __run_tasks():
221
- while True:
222
- [Thread(target=__run_task, args=(task,)).start() for task in self.tasks[:]]
223
- time.sleep(1)
224
-
204
+ """Call once to start background task processing."""
225
205
  if self._initialized is False:
226
206
  self._initialized = True
227
- Thread(target=__run_tasks, daemon=True).start()
207
+ Thread(target=self._run_tasks, daemon=True).start()
208
+
209
+ def _run_task(self, task: BackgroundTask):
210
+ should_continue = task()
211
+ if should_continue is False:
212
+ with self._lock:
213
+ if task in self.tasks:
214
+ self.tasks.remove(task)
215
+
216
+ def _run_tasks(self):
217
+ while True:
218
+ with self._lock:
219
+ tasks_snapshot = self.tasks[:]
220
+ for task in tasks_snapshot:
221
+ Thread(target=self._run_task, args=(task,)).start()
222
+ time.sleep(1)
228
223
 
229
224
  @classmethod
230
- def _is_instance_of_task(cls, task, /):
225
+ def _is_instance_of_task(cls, task: Any, /) -> bool:
231
226
  if not isinstance(task, BackgroundTask):
232
227
  name = getattr(task, '__name__', task.__class__.__name__)
233
228
  logger.error(f'`{name}` should be instance of `background_tasks.BackgroundTask`')
@@ -235,25 +230,4 @@ class BackgroundTasks(Singleton):
235
230
  return True
236
231
 
237
232
 
238
- background_tasks = BackgroundTasks()
239
-
240
- """
241
- -------------------------------------------------------------
242
- Example:
243
- -------------------------------------------------------------
244
- >>> import datetime
245
-
246
-
247
- >>> async def hello(name: str):
248
- >>> print(f'Hello {name}')
249
-
250
- # Run it every 5 seconds for 2 times
251
-
252
- >>> task1 = BackgroundTask(hello, 'Ali').interval(2).every_seconds(5)
253
- >>> background_tasks.add_task(task1)
254
-
255
- # Run it every day at 08:00 O'clock forever
256
-
257
- >>> task2 = BackgroundTask(hello, 'Saba').interval(-1).every_days().at(datetime.time(hour=8))
258
- >>> background_tasks.add_task(task2)
259
- """
233
+ _background_tasks = BackgroundTasks()
panther/base_request.py CHANGED
@@ -1,9 +1,12 @@
1
+ import typing
1
2
  from collections.abc import Callable
2
3
  from urllib.parse import parse_qsl
3
4
 
4
- from panther.db import Model
5
5
  from panther.exceptions import InvalidPathVariableAPIError
6
6
 
7
+ if typing.TYPE_CHECKING:
8
+ from panther.db import Model
9
+
7
10
 
8
11
  class Headers:
9
12
  accept: str
@@ -56,10 +59,10 @@ class Headers:
56
59
 
57
60
  def get_cookies(self) -> dict:
58
61
  """
59
- request.headers.cookie:
62
+ Example of `request.headers.cookie`:
60
63
  'csrftoken=aaa; sessionid=bbb; access_token=ccc; refresh_token=ddd'
61
64
 
62
- request.headers.get_cookies():
65
+ Example of `request.headers.get_cookies()`:
63
66
  {
64
67
  'csrftoken': 'aaa',
65
68
  'sessionid': 'bbb',
@@ -126,17 +129,14 @@ class BaseRequest:
126
129
  def collect_path_variables(self, found_path: str):
127
130
  self.path_variables = {
128
131
  variable.strip('< >'): value
129
- for variable, value in zip(
130
- found_path.strip('/').split('/'),
131
- self.path.strip('/').split('/')
132
- )
132
+ for variable, value in zip(found_path.strip('/').split('/'), self.path.strip('/').split('/'))
133
133
  if variable.startswith('<')
134
134
  }
135
135
 
136
- def clean_parameters(self, func: Callable) -> dict:
136
+ def clean_parameters(self, function_annotations: dict) -> dict:
137
137
  kwargs = self.path_variables.copy()
138
138
 
139
- for variable_name, variable_type in func.__annotations__.items():
139
+ for variable_name, variable_type in function_annotations.items():
140
140
  # Put Request/ Websocket In kwargs (If User Wants It)
141
141
  if issubclass(variable_type, BaseRequest):
142
142
  kwargs[variable_name] = self
@@ -144,7 +144,13 @@ class BaseRequest:
144
144
  elif variable_name in kwargs:
145
145
  # Cast To Boolean
146
146
  if variable_type is bool:
147
- kwargs[variable_name] = kwargs[variable_name].lower() not in ['false', '0']
147
+ value = kwargs[variable_name].lower()
148
+ if value in ['false', '0']:
149
+ kwargs[variable_name] = False
150
+ elif value in ['true', '1']:
151
+ kwargs[variable_name] = True
152
+ else:
153
+ raise InvalidPathVariableAPIError(value=kwargs[variable_name], variable_type=variable_type)
148
154
 
149
155
  # Cast To Int
150
156
  elif variable_type is int:
panther/base_websocket.py CHANGED
@@ -12,7 +12,7 @@ from panther import status
12
12
  from panther.base_request import BaseRequest
13
13
  from panther.configs import config
14
14
  from panther.db.connections import redis
15
- from panther.exceptions import InvalidPathVariableAPIError, BaseError
15
+ from panther.exceptions import BaseError, InvalidPathVariableAPIError
16
16
  from panther.utils import Singleton
17
17
 
18
18
  if TYPE_CHECKING:
@@ -83,11 +83,11 @@ class WebsocketConnections(Singleton):
83
83
 
84
84
  async def _handle_received_message(self, received_message):
85
85
  if (
86
- isinstance(received_message, dict)
87
- and (connection_id := received_message.get('connection_id'))
88
- and connection_id in self.connections
89
- and 'action' in received_message
90
- and 'data' in received_message
86
+ isinstance(received_message, dict)
87
+ and (connection_id := received_message.get('connection_id'))
88
+ and connection_id in self.connections
89
+ and 'action' in received_message
90
+ and 'data' in received_message
91
91
  ):
92
92
  # Check Action of WS
93
93
  match received_message['action']:
@@ -96,7 +96,7 @@ class WebsocketConnections(Singleton):
96
96
  case 'close':
97
97
  await self.connections[connection_id].close(
98
98
  code=received_message['data']['code'],
99
- reason=received_message['data']['reason']
99
+ reason=received_message['data']['reason'],
100
100
  )
101
101
  case unknown_action:
102
102
  logger.error(f'Unknown Message Action: {unknown_action}')
@@ -124,7 +124,7 @@ class WebsocketConnections(Singleton):
124
124
 
125
125
  # 3. Put PathVariables and Request(If User Wants It) In kwargs
126
126
  try:
127
- kwargs = connection.clean_parameters(connection.connect)
127
+ kwargs = connection.clean_parameters(connection.connect.__annotations__)
128
128
  except InvalidPathVariableAPIError as e:
129
129
  connection.change_state(state='Rejected', message=e.detail)
130
130
  return await connection.close()
panther/caching.py CHANGED
@@ -1,42 +1,33 @@
1
- from collections import namedtuple
2
- from datetime import timedelta, datetime
3
1
  import logging
4
- from types import NoneType
2
+ from collections import namedtuple
3
+ from datetime import datetime, timedelta
5
4
 
6
5
  import orjson as json
7
6
 
8
- from panther.configs import config
9
7
  from panther.db.connections import redis
10
8
  from panther.request import Request
11
9
  from panther.response import Response
12
- from panther.throttling import throttling_storage
13
10
  from panther.utils import generate_hash_value_from_string, round_datetime
14
11
 
15
12
  logger = logging.getLogger('panther')
16
13
 
17
- caches = {}
14
+ caches: dict[str, tuple[bytes, dict, int]] = {}
18
15
  CachedResponse = namedtuple('CachedResponse', ['data', 'headers', 'status_code'])
19
16
 
20
17
 
21
- def api_cache_key(request: Request, cache_exp_time: timedelta | None = None) -> str:
22
- client = request.user and request.user.id or request.client.ip
18
+ def api_cache_key(request: Request, duration: timedelta | None = None) -> str:
19
+ client = (request.user and request.user.id) or request.client.ip
23
20
  query_params_hash = generate_hash_value_from_string(request.scope['query_string'].decode('utf-8'))
24
21
  key = f'{client}-{request.path}-{query_params_hash}-{request.validated_data}'
25
22
 
26
- if cache_exp_time:
27
- time = round_datetime(datetime.now(), cache_exp_time)
23
+ if duration:
24
+ time = round_datetime(datetime.now(), duration)
28
25
  return f'{time}-{key}'
29
26
 
30
27
  return key
31
28
 
32
29
 
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:
30
+ async def get_response_from_cache(*, request: Request, duration: timedelta) -> CachedResponse | None:
40
31
  """
41
32
  If redis.is_connected:
42
33
  Get Cached Data From Redis
@@ -47,18 +38,14 @@ async def get_response_from_cache(*, request: Request, cache_exp_time: timedelta
47
38
  key = api_cache_key(request=request)
48
39
  data = (await redis.get(key) or b'{}').decode()
49
40
  if value := json.loads(data):
50
- return CachedResponse(
51
- data=value[0].encode(),
52
- headers=value[1],
53
- status_code=value[2]
54
- )
41
+ return CachedResponse(data=value[0].encode(), headers=value[1], status_code=value[2])
55
42
  else:
56
- key = api_cache_key(request=request, cache_exp_time=cache_exp_time)
43
+ key = api_cache_key(request=request, duration=duration)
57
44
  if value := caches.get(key):
58
45
  return CachedResponse(*value)
59
46
 
60
47
 
61
- async def set_response_in_cache(*, request: Request, response: Response, cache_exp_time: timedelta | int) -> None:
48
+ async def set_response_in_cache(*, request: Request, response: Response, duration: timedelta | int) -> None:
62
49
  """
63
50
  If redis.is_connected:
64
51
  Cache The Data In Redis
@@ -68,61 +55,10 @@ async def set_response_in_cache(*, request: Request, response: Response, cache_e
68
55
 
69
56
  if redis.is_connected:
70
57
  key = api_cache_key(request=request)
71
- cache_data: tuple[str, str, int] = (response.body.decode(), response.headers, response.status_code)
72
- cache_exp_time = cache_exp_time or config.DEFAULT_CACHE_EXP
73
- cache_data: bytes = json.dumps(cache_data)
74
-
75
- if not isinstance(cache_exp_time, timedelta | int | NoneType):
76
- msg = '`cache_exp_time` should be instance of `datetime.timedelta`, `int` or `None`'
77
- raise TypeError(msg)
78
-
79
- if cache_exp_time is None:
80
- logger.warning(
81
- 'your response are going to cache in redis forever '
82
- '** set DEFAULT_CACHE_EXP in `configs` or set the `cache_exp_time` in `@API.get()` to prevent this **'
83
- )
84
- await redis.set(key, cache_data)
85
- else:
86
- await redis.set(key, cache_data, ex=cache_exp_time)
87
-
88
- else:
89
- key = api_cache_key(request=request, cache_exp_time=cache_exp_time)
90
- cache_data: tuple[bytes, str, int] = (response.body, response.headers, response.status_code)
91
-
92
- caches[key] = cache_data
93
-
94
- if cache_exp_time:
95
- logger.info('`cache_exp_time` is not very accurate when `redis` is not connected.')
96
-
97
-
98
- async def get_throttling_from_cache(request: Request, duration: timedelta) -> int:
99
- """
100
- If redis.is_connected:
101
- Get Cached Data From Redis
102
- else:
103
- Get Cached Data From Memory
104
- """
105
- key = throttling_cache_key(request=request, duration=duration)
106
-
107
- if redis.is_connected:
108
- data = (await redis.get(key) or b'0').decode()
109
- return json.loads(data)
110
-
111
- else:
112
- return throttling_storage[key]
113
-
114
-
115
- async def increment_throttling_in_cache(request: Request, duration: timedelta) -> None:
116
- """
117
- If redis.is_connected:
118
- Increment The Data In Redis
119
- else:
120
- Increment The Data In Memory
121
- """
122
- key = throttling_cache_key(request=request, duration=duration)
123
-
124
- if redis.is_connected:
125
- await redis.incrby(key, amount=1)
58
+ cache_data: tuple[str, dict, int] = (response.body.decode(), response.headers, response.status_code)
59
+ await redis.set(key, json.dumps(cache_data), ex=duration)
126
60
 
127
61
  else:
128
- throttling_storage[key] += 1
62
+ key = api_cache_key(request=request, duration=duration)
63
+ caches[key] = (response.body, response.headers, response.status_code)
64
+ logger.info('`cache` is not very accurate when `redis` is not connected.')
@@ -1,5 +1,5 @@
1
+ from collections.abc import Callable
1
2
  from pathlib import Path
2
- from typing import Callable
3
3
 
4
4
  from rich import print as rich_print
5
5
  from rich.console import Console
@@ -8,15 +8,16 @@ from rich.prompt import Prompt
8
8
 
9
9
  from panther import version
10
10
  from panther.cli.template import (
11
- TEMPLATE,
12
- SINGLE_FILE_TEMPLATE,
13
11
  AUTHENTICATION_PART,
14
- MONITORING_PART,
15
- LOG_QUERIES_PART,
16
12
  AUTO_REFORMAT_PART,
17
- DATABASE_PANTHERDB_PART,
18
13
  DATABASE_MONGODB_PART,
19
- USER_MODEL_PART, REDIS_PART,
14
+ DATABASE_PANTHERDB_PART,
15
+ LOG_QUERIES_PART,
16
+ MONITORING_PART,
17
+ REDIS_PART,
18
+ SINGLE_FILE_TEMPLATE,
19
+ TEMPLATE,
20
+ USER_MODEL_PART,
20
21
  )
21
22
  from panther.cli.utils import cli_error
22
23
 
@@ -52,7 +53,7 @@ class CreateProject:
52
53
  'message': 'Directory (default is .)',
53
54
  'validation_func': self._check_all_directories,
54
55
  'error_message': '"{}" Directory Already Exists.',
55
- 'show_validation_error': True
56
+ 'show_validation_error': True,
56
57
  },
57
58
  {
58
59
  'field': 'single_file',
@@ -69,7 +70,7 @@ class CreateProject:
69
70
  'field': 'database_encryption',
70
71
  'message': 'Do You Want Encryption For Your Database (Required `cryptography`)',
71
72
  'is_boolean': True,
72
- 'condition': "self.database == '0'"
73
+ 'condition': "self.database == '0'",
73
74
  },
74
75
  {
75
76
  'field': 'redis',
@@ -90,7 +91,7 @@ class CreateProject:
90
91
  'field': 'log_queries',
91
92
  'message': 'Do You Want To Log Queries',
92
93
  'is_boolean': True,
93
- 'condition': "self.database != '2'"
94
+ 'condition': "self.database != '2'",
94
95
  },
95
96
  {
96
97
  'field': 'auto_reformat',
@@ -194,12 +195,12 @@ class CreateProject:
194
195
  self.progress(i + 1)
195
196
 
196
197
  def ask(
197
- self,
198
- message: str,
199
- default: str | bool,
200
- error_message: str,
201
- validation_func: Callable,
202
- show_validation_error: bool = False,
198
+ self,
199
+ message: str,
200
+ default: str | bool,
201
+ error_message: str,
202
+ validation_func: Callable,
203
+ show_validation_error: bool = False,
203
204
  ) -> str:
204
205
  value = Prompt.ask(message, console=self.input_console).lower() or default
205
206
  while not validation_func(value):
panther/cli/main.py CHANGED
@@ -15,7 +15,7 @@ def shell(args) -> None:
15
15
  return cli_error(
16
16
  'Not Enough Arguments, Give me a file path that contains `Panther()` app.\n'
17
17
  ' * Make sure to run `panther shell` in the same directory as that file!\n'
18
- ' * Example: `panther shell main.py`'
18
+ ' * Example: `panther shell main.py`',
19
19
  )
20
20
  elif len(args) != 1:
21
21
  return cli_error('Too Many Arguments.')