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.
- panther/__init__.py +1 -1
- panther/_load_configs.py +46 -37
- panther/_utils.py +49 -34
- panther/app.py +96 -97
- panther/authentications.py +97 -50
- panther/background_tasks.py +98 -124
- panther/base_request.py +16 -10
- panther/base_websocket.py +8 -8
- panther/caching.py +16 -80
- panther/cli/create_command.py +17 -16
- panther/cli/main.py +1 -1
- panther/cli/monitor_command.py +11 -6
- panther/cli/run_command.py +5 -71
- panther/cli/template.py +7 -7
- panther/cli/utils.py +58 -69
- panther/configs.py +70 -72
- panther/db/connections.py +30 -24
- panther/db/cursor.py +3 -1
- panther/db/models.py +26 -10
- panther/db/queries/base_queries.py +4 -5
- panther/db/queries/mongodb_queries.py +21 -21
- panther/db/queries/pantherdb_queries.py +1 -1
- panther/db/queries/queries.py +26 -8
- panther/db/utils.py +1 -1
- panther/events.py +25 -14
- panther/exceptions.py +2 -7
- panther/file_handler.py +1 -1
- panther/generics.py +74 -100
- panther/logging.py +2 -1
- panther/main.py +12 -13
- panther/middlewares/cors.py +67 -0
- panther/middlewares/monitoring.py +5 -3
- panther/openapi/urls.py +2 -2
- panther/openapi/utils.py +3 -3
- panther/openapi/views.py +20 -37
- panther/pagination.py +4 -2
- panther/panel/apis.py +2 -7
- panther/panel/urls.py +2 -6
- panther/panel/utils.py +9 -5
- panther/panel/views.py +13 -22
- panther/permissions.py +2 -1
- panther/request.py +2 -1
- panther/response.py +101 -94
- panther/routings.py +12 -12
- panther/serializer.py +20 -43
- panther/test.py +73 -58
- panther/throttling.py +68 -3
- panther/utils.py +5 -11
- panther-5.0.0b5.dist-info/METADATA +188 -0
- panther-5.0.0b5.dist-info/RECORD +75 -0
- panther/monitoring.py +0 -34
- panther-5.0.0b3.dist-info/METADATA +0 -223
- panther-5.0.0b3.dist-info/RECORD +0 -75
- {panther-5.0.0b3.dist-info → panther-5.0.0b5.dist-info}/WHEEL +0 -0
- {panther-5.0.0b3.dist-info → panther-5.0.0b5.dist-info}/entry_points.txt +0 -0
- {panther-5.0.0b3.dist-info → panther-5.0.0b5.dist-info}/licenses/LICENSE +0 -0
- {panther-5.0.0b3.dist-info → panther-5.0.0b5.dist-info}/top_level.txt +0 -0
panther/background_tasks.py
CHANGED
@@ -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
|
7
|
-
from
|
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
|
-
|
13
|
-
|
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
|
-
|
32
|
-
|
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
|
-
|
35
|
-
|
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:
|
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,
|
110
|
+
Set day to schedule the task. Accepts string like 'monday', 'tuesday', etc.
|
99
111
|
"""
|
100
|
-
|
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
|
-
|
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 =
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
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
|
-
|
209
|
-
|
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=
|
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
|
-
|
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,
|
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
|
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
|
-
|
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
|
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
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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
|
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,
|
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
|
27
|
-
time = round_datetime(datetime.now(),
|
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
|
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,
|
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,
|
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,
|
72
|
-
|
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
|
-
|
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.')
|
panther/cli/create_command.py
CHANGED
@@ -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
|
-
|
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
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
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.')
|