panther 5.0.0b3__py3-none-any.whl → 5.0.0b4__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 +18 -24
- panther/db/cursor.py +0 -1
- panther/db/models.py +24 -8
- panther/db/queries/base_queries.py +2 -5
- panther/db/queries/mongodb_queries.py +17 -20
- 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 +11 -8
- 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 +53 -47
- panther/routings.py +12 -12
- panther/serializer.py +19 -20
- panther/test.py +73 -58
- panther/throttling.py +68 -3
- panther/utils.py +5 -11
- {panther-5.0.0b3.dist-info → panther-5.0.0b4.dist-info}/METADATA +1 -1
- panther-5.0.0b4.dist-info/RECORD +75 -0
- panther/monitoring.py +0 -34
- panther-5.0.0b3.dist-info/RECORD +0 -75
- {panther-5.0.0b3.dist-info → panther-5.0.0b4.dist-info}/WHEEL +0 -0
- {panther-5.0.0b3.dist-info → panther-5.0.0b4.dist-info}/entry_points.txt +0 -0
- {panther-5.0.0b3.dist-info → panther-5.0.0b4.dist-info}/licenses/LICENSE +0 -0
- {panther-5.0.0b3.dist-info → panther-5.0.0b4.dist-info}/top_level.txt +0 -0
panther/authentications.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
import logging
|
2
2
|
import time
|
3
3
|
from abc import abstractmethod
|
4
|
-
from datetime import
|
4
|
+
from datetime import datetime, timezone
|
5
5
|
from typing import Literal
|
6
6
|
|
7
7
|
from panther.base_websocket import Websocket
|
@@ -36,24 +36,44 @@ class BaseAuthentication:
|
|
36
36
|
|
37
37
|
|
38
38
|
class JWTAuthentication(BaseAuthentication):
|
39
|
-
|
39
|
+
"""
|
40
|
+
Retrieve the Authorization from header
|
41
|
+
Example:
|
42
|
+
Headers: {'authorization': 'Bearer the_jwt_token'}
|
43
|
+
"""
|
44
|
+
|
45
|
+
model = None
|
40
46
|
keyword = 'Bearer'
|
41
47
|
algorithm = 'HS256'
|
42
48
|
HTTP_HEADER_ENCODING = 'iso-8859-1' # RFC5987
|
43
49
|
|
44
50
|
@classmethod
|
45
|
-
def get_authorization_header(cls, request: Request | Websocket) -> str:
|
51
|
+
def get_authorization_header(cls, request: Request | Websocket) -> list[str]:
|
52
|
+
"""Retrieve the Authorization header from the request."""
|
46
53
|
if auth := request.headers.authorization:
|
47
|
-
return auth
|
54
|
+
return auth.split()
|
48
55
|
msg = 'Authorization is required'
|
49
56
|
raise cls.exception(msg) from None
|
50
57
|
|
51
58
|
@classmethod
|
52
59
|
async def authentication(cls, request: Request | Websocket) -> Model:
|
53
|
-
|
60
|
+
"""Authenticate the user based on the JWT token in the Authorization header."""
|
61
|
+
auth_header = cls.get_authorization_header(request)
|
62
|
+
token = cls.get_token(auth_header=auth_header)
|
63
|
+
|
64
|
+
if redis.is_connected and await cls.is_token_revoked(token=token):
|
65
|
+
msg = 'User logged out'
|
66
|
+
raise cls.exception(msg) from None
|
54
67
|
|
68
|
+
payload = await cls.decode_jwt(token)
|
69
|
+
user = await cls.get_user(payload)
|
70
|
+
user._auth_token = token
|
71
|
+
return user
|
72
|
+
|
73
|
+
@classmethod
|
74
|
+
def get_token(cls, auth_header):
|
55
75
|
if len(auth_header) != 2:
|
56
|
-
msg = 'Authorization
|
76
|
+
msg = 'Authorization header must contain 2 parts'
|
57
77
|
raise cls.exception(msg) from None
|
58
78
|
|
59
79
|
bearer, token = auth_header
|
@@ -67,18 +87,11 @@ class JWTAuthentication(BaseAuthentication):
|
|
67
87
|
msg = 'Authorization keyword is not valid'
|
68
88
|
raise cls.exception(msg) from None
|
69
89
|
|
70
|
-
|
71
|
-
msg = 'User logged out'
|
72
|
-
raise cls.exception(msg) from None
|
73
|
-
|
74
|
-
payload = cls.decode_jwt(token)
|
75
|
-
user = await cls.get_user(payload)
|
76
|
-
user._auth_token = token
|
77
|
-
return user
|
90
|
+
return token
|
78
91
|
|
79
92
|
@classmethod
|
80
|
-
def decode_jwt(cls, token: str) -> dict:
|
81
|
-
"""Decode JWT token
|
93
|
+
async def decode_jwt(cls, token: str) -> dict:
|
94
|
+
"""Decode a JWT token and return the payload."""
|
82
95
|
try:
|
83
96
|
return jwt.decode(
|
84
97
|
token=token,
|
@@ -90,21 +103,21 @@ class JWTAuthentication(BaseAuthentication):
|
|
90
103
|
|
91
104
|
@classmethod
|
92
105
|
async def get_user(cls, payload: dict) -> Model:
|
93
|
-
"""
|
106
|
+
"""Fetch the user based on the decoded JWT payload from cls.model or config.UserModel"""
|
94
107
|
if (user_id := payload.get('user_id')) is None:
|
95
108
|
msg = 'Payload does not have `user_id`'
|
96
109
|
raise cls.exception(msg)
|
97
110
|
|
98
|
-
user_model =
|
99
|
-
|
100
|
-
|
111
|
+
user_model = cls.model or config.USER_MODEL
|
112
|
+
user = await user_model.find_one(id=user_id)
|
113
|
+
if user is None:
|
114
|
+
raise cls.exception('User not found')
|
101
115
|
|
102
|
-
|
103
|
-
raise cls.exception(msg) from None
|
116
|
+
return user
|
104
117
|
|
105
118
|
@classmethod
|
106
119
|
def encode_jwt(cls, user_id: str, token_type: Literal['access', 'refresh'] = 'access') -> str:
|
107
|
-
"""
|
120
|
+
"""Generate a JWT token for a given user ID."""
|
108
121
|
issued_at = datetime.now(timezone.utc).timestamp()
|
109
122
|
if token_type == 'access':
|
110
123
|
expire = issued_at + config.JWT_CONFIG.life_time
|
@@ -124,44 +137,87 @@ class JWTAuthentication(BaseAuthentication):
|
|
124
137
|
)
|
125
138
|
|
126
139
|
@classmethod
|
127
|
-
def login(cls,
|
128
|
-
"""
|
140
|
+
async def login(cls, user) -> dict:
|
141
|
+
"""Generate access and refresh tokens for user login."""
|
129
142
|
return {
|
130
|
-
'access_token': cls.encode_jwt(user_id=
|
131
|
-
'refresh_token': cls.encode_jwt(user_id=
|
143
|
+
'access_token': cls.encode_jwt(user_id=user.id),
|
144
|
+
'refresh_token': cls.encode_jwt(user_id=user.id, token_type='refresh'),
|
132
145
|
}
|
133
146
|
|
134
147
|
@classmethod
|
135
|
-
async def logout(cls,
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
148
|
+
async def logout(cls, user) -> None:
|
149
|
+
"""Log out a user by revoking their JWT token."""
|
150
|
+
payload = await cls.decode_jwt(token=user._auth_token)
|
151
|
+
await cls.revoke_token_in_cache(token=user._auth_token, exp=payload['exp'])
|
152
|
+
|
153
|
+
@classmethod
|
154
|
+
async def refresh(cls, user):
|
155
|
+
if hasattr(user, '_auth_refresh_token'):
|
156
|
+
# It happens in CookieJWTAuthentication
|
157
|
+
token = user._auth_refresh_token
|
141
158
|
else:
|
142
|
-
|
159
|
+
token = user._auth_token
|
160
|
+
|
161
|
+
payload = await cls.decode_jwt(token=token)
|
162
|
+
|
163
|
+
if payload['token_type'] != 'refresh':
|
164
|
+
raise cls.exception('Invalid token type; expected `refresh` token.')
|
165
|
+
# Revoke after use
|
166
|
+
await cls.revoke_token_in_cache(token=token, exp=payload['exp'])
|
167
|
+
|
168
|
+
return await cls.login(user=user)
|
143
169
|
|
144
170
|
@classmethod
|
145
|
-
async def
|
146
|
-
|
147
|
-
|
171
|
+
async def revoke_token_in_cache(cls, token: str, exp: int) -> None:
|
172
|
+
"""Mark the token as revoked in the cache."""
|
173
|
+
if redis.is_connected:
|
174
|
+
key = generate_hash_value_from_string(token)
|
175
|
+
remaining_exp_time = int(exp - time.time())
|
176
|
+
await redis.set(key, b'', ex=remaining_exp_time)
|
177
|
+
else:
|
178
|
+
logger.error('Redis is not connected; token revocation is not effective.')
|
148
179
|
|
149
180
|
@classmethod
|
150
|
-
async def
|
181
|
+
async def is_token_revoked(cls, token: str) -> bool:
|
182
|
+
"""Check if the token is revoked by looking it up in the cache."""
|
151
183
|
key = generate_hash_value_from_string(token)
|
152
184
|
return bool(await redis.exists(key))
|
153
185
|
|
154
186
|
|
155
187
|
class QueryParamJWTAuthentication(JWTAuthentication):
|
188
|
+
"""
|
189
|
+
Retrieve the Authorization from query params
|
190
|
+
Example:
|
191
|
+
https://example.com?authorization=the_jwt_without_bearer
|
192
|
+
"""
|
193
|
+
|
156
194
|
@classmethod
|
157
|
-
def get_authorization_header(cls, request: Request | Websocket) -> str:
|
195
|
+
def get_authorization_header(cls, request: Request | Websocket) -> list[str]:
|
158
196
|
if auth := request.query_params.get('authorization'):
|
159
197
|
return auth
|
160
198
|
msg = '`authorization` query param not found.'
|
161
199
|
raise cls.exception(msg) from None
|
162
200
|
|
201
|
+
@classmethod
|
202
|
+
def get_token(cls, auth_header) -> str:
|
203
|
+
return auth_header
|
204
|
+
|
163
205
|
|
164
206
|
class CookieJWTAuthentication(JWTAuthentication):
|
207
|
+
"""
|
208
|
+
Retrieve the Authorization from cookies
|
209
|
+
Example:
|
210
|
+
Cookies: access_token=the_jwt_without_bearer
|
211
|
+
"""
|
212
|
+
|
213
|
+
@classmethod
|
214
|
+
async def authentication(cls, request: Request | Websocket) -> Model:
|
215
|
+
user = await super().authentication(request=request)
|
216
|
+
if refresh_token := request.headers.get_cookies().get('refresh_token'):
|
217
|
+
# It's used in `cls.refresh()`
|
218
|
+
user._auth_refresh_token = refresh_token
|
219
|
+
return user
|
220
|
+
|
165
221
|
@classmethod
|
166
222
|
def get_authorization_header(cls, request: Request | Websocket) -> str:
|
167
223
|
if token := request.headers.get_cookies().get('access_token'):
|
@@ -170,14 +226,5 @@ class CookieJWTAuthentication(JWTAuthentication):
|
|
170
226
|
raise cls.exception(msg) from None
|
171
227
|
|
172
228
|
@classmethod
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
if redis.is_connected and await cls._check_in_cache(token=token):
|
177
|
-
msg = 'User logged out'
|
178
|
-
raise cls.exception(msg) from None
|
179
|
-
|
180
|
-
payload = cls.decode_jwt(token)
|
181
|
-
user = await cls.get_user(payload)
|
182
|
-
user._auth_token = token
|
183
|
-
return user
|
229
|
+
def get_token(cls, auth_header) -> str:
|
230
|
+
return auth_header
|
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:
|