panther 3.8.2__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.
- panther/__init__.py +1 -1
- panther/_load_configs.py +168 -171
- panther/_utils.py +26 -49
- panther/app.py +85 -105
- panther/authentications.py +86 -55
- panther/background_tasks.py +25 -14
- panther/base_request.py +38 -14
- panther/base_websocket.py +172 -94
- panther/caching.py +60 -25
- panther/cli/create_command.py +20 -10
- panther/cli/monitor_command.py +63 -37
- panther/cli/template.py +40 -20
- panther/cli/utils.py +32 -18
- panther/configs.py +65 -58
- panther/db/connections.py +139 -0
- panther/db/cursor.py +43 -0
- panther/db/models.py +64 -29
- panther/db/queries/__init__.py +1 -1
- panther/db/queries/base_queries.py +127 -0
- panther/db/queries/mongodb_queries.py +77 -38
- panther/db/queries/pantherdb_queries.py +59 -30
- panther/db/queries/queries.py +232 -117
- panther/db/utils.py +17 -18
- panther/events.py +44 -0
- panther/exceptions.py +26 -12
- panther/file_handler.py +2 -2
- panther/generics.py +163 -0
- panther/logging.py +7 -2
- panther/main.py +111 -188
- panther/middlewares/base.py +3 -0
- panther/monitoring.py +8 -5
- panther/pagination.py +48 -0
- panther/panel/apis.py +32 -5
- panther/panel/urls.py +2 -1
- panther/permissions.py +3 -3
- panther/request.py +6 -13
- panther/response.py +114 -34
- panther/routings.py +83 -66
- panther/serializer.py +214 -33
- panther/test.py +31 -21
- panther/utils.py +28 -16
- panther/websocket.py +7 -4
- {panther-3.8.2.dist-info → panther-4.0.0.dist-info}/METADATA +93 -71
- panther-4.0.0.dist-info/RECORD +57 -0
- {panther-3.8.2.dist-info → panther-4.0.0.dist-info}/WHEEL +1 -1
- panther/db/connection.py +0 -92
- panther/middlewares/db.py +0 -18
- panther/middlewares/redis.py +0 -47
- panther-3.8.2.dist-info/RECORD +0 -54
- {panther-3.8.2.dist-info → panther-4.0.0.dist-info}/LICENSE +0 -0
- {panther-3.8.2.dist-info → panther-4.0.0.dist-info}/entry_points.txt +0 -0
- {panther-3.8.2.dist-info → panther-4.0.0.dist-info}/top_level.txt +0 -0
panther/app.py
CHANGED
@@ -1,32 +1,34 @@
|
|
1
1
|
import functools
|
2
|
-
from collections.abc import Callable
|
3
|
-
from datetime import datetime, timedelta
|
4
2
|
import logging
|
3
|
+
from datetime import timedelta
|
5
4
|
from typing import Literal
|
6
5
|
|
7
6
|
from orjson import JSONDecodeError
|
8
|
-
from pydantic import ValidationError
|
7
|
+
from pydantic import ValidationError, BaseModel
|
9
8
|
|
10
|
-
from panther import status
|
11
9
|
from panther._utils import is_function_async
|
12
|
-
from panther.caching import
|
10
|
+
from panther.caching import (
|
11
|
+
get_response_from_cache,
|
12
|
+
set_response_in_cache,
|
13
|
+
get_throttling_from_cache,
|
14
|
+
increment_throttling_in_cache
|
15
|
+
)
|
13
16
|
from panther.configs import config
|
14
17
|
from panther.exceptions import (
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
18
|
+
APIError,
|
19
|
+
AuthorizationAPIError,
|
20
|
+
JSONDecodeAPIError,
|
21
|
+
MethodNotAllowedAPIError,
|
22
|
+
ThrottlingAPIError,
|
23
|
+
BadRequestAPIError
|
21
24
|
)
|
22
25
|
from panther.request import Request
|
23
26
|
from panther.response import Response
|
24
|
-
from panther.
|
25
|
-
from panther.
|
27
|
+
from panther.serializer import ModelSerializer
|
28
|
+
from panther.throttling import Throttling
|
26
29
|
|
27
30
|
__all__ = ('API', 'GenericAPI')
|
28
31
|
|
29
|
-
|
30
32
|
logger = logging.getLogger('panther')
|
31
33
|
|
32
34
|
|
@@ -34,11 +36,11 @@ class API:
|
|
34
36
|
def __init__(
|
35
37
|
self,
|
36
38
|
*,
|
37
|
-
input_model=None,
|
38
|
-
output_model=None,
|
39
|
+
input_model: type[ModelSerializer] | type[BaseModel] | None = None,
|
40
|
+
output_model: type[ModelSerializer] | type[BaseModel] | None = None,
|
39
41
|
auth: bool = False,
|
40
42
|
permissions: list | None = None,
|
41
|
-
throttling: Throttling = None,
|
43
|
+
throttling: Throttling | None = None,
|
42
44
|
cache: bool = False,
|
43
45
|
cache_exp_time: timedelta | int | None = None,
|
44
46
|
methods: list[Literal['GET', 'POST', 'PUT', 'PATCH', 'DELETE']] | None = None,
|
@@ -55,21 +57,21 @@ class API:
|
|
55
57
|
|
56
58
|
def __call__(self, func):
|
57
59
|
@functools.wraps(func)
|
58
|
-
async def wrapper(request: Request
|
59
|
-
self.request
|
60
|
+
async def wrapper(request: Request) -> Response:
|
61
|
+
self.request = request
|
60
62
|
|
61
63
|
# 1. Check Method
|
62
64
|
if self.methods and self.request.method not in self.methods:
|
63
|
-
raise
|
65
|
+
raise MethodNotAllowedAPIError
|
64
66
|
|
65
67
|
# 2. Authentication
|
66
|
-
self.
|
68
|
+
await self.handle_authentication()
|
67
69
|
|
68
|
-
# 3.
|
69
|
-
self.
|
70
|
+
# 3. Permissions
|
71
|
+
await self.handle_permission()
|
70
72
|
|
71
|
-
# 4.
|
72
|
-
self.
|
73
|
+
# 4. Throttling
|
74
|
+
await self.handle_throttling()
|
73
75
|
|
74
76
|
# 5. Validate Input
|
75
77
|
if self.request.method in ['POST', 'PUT', 'PATCH']:
|
@@ -77,37 +79,33 @@ class API:
|
|
77
79
|
|
78
80
|
# 6. Get Cached Response
|
79
81
|
if self.cache and self.request.method == 'GET':
|
80
|
-
if cached :=
|
82
|
+
if cached := await get_response_from_cache(request=self.request, cache_exp_time=self.cache_exp_time):
|
81
83
|
return Response(data=cached.data, status_code=cached.status_code)
|
82
84
|
|
83
|
-
# 7.
|
84
|
-
self.
|
85
|
-
|
86
|
-
# 8. Put Request In kwargs (If User Wants It)
|
87
|
-
kwargs = {}
|
88
|
-
if req_arg := [k for k, v in func.__annotations__.items() if v == Request]:
|
89
|
-
kwargs[req_arg[0]] = self.request
|
85
|
+
# 7. Put PathVariables and Request(If User Wants It) In kwargs
|
86
|
+
kwargs = self.request.clean_parameters(func)
|
90
87
|
|
91
|
-
#
|
88
|
+
# 8. Call Endpoint
|
92
89
|
if is_function_async(func):
|
93
|
-
response = await func(**kwargs
|
90
|
+
response = await func(**kwargs)
|
94
91
|
else:
|
95
|
-
response = func(**kwargs
|
92
|
+
response = func(**kwargs)
|
96
93
|
|
97
|
-
#
|
94
|
+
# 9. Clean Response
|
98
95
|
if not isinstance(response, Response):
|
99
96
|
response = Response(data=response)
|
100
|
-
|
97
|
+
if self.output_model and response.data:
|
98
|
+
response.data = response.apply_output_model(response.data, output_model=self.output_model)
|
101
99
|
|
102
|
-
#
|
100
|
+
# 10. Set New Response To Cache
|
103
101
|
if self.cache and self.request.method == 'GET':
|
104
|
-
|
102
|
+
await set_response_in_cache(
|
105
103
|
request=self.request,
|
106
104
|
response=response,
|
107
105
|
cache_exp_time=self.cache_exp_time
|
108
106
|
)
|
109
107
|
|
110
|
-
#
|
108
|
+
# 11. Warning CacheExpTime
|
111
109
|
if self.cache_exp_time and self.cache is False:
|
112
110
|
logger.warning('"cache_exp_time" won\'t work while "cache" is False')
|
113
111
|
|
@@ -115,70 +113,51 @@ class API:
|
|
115
113
|
|
116
114
|
return wrapper
|
117
115
|
|
118
|
-
def
|
119
|
-
auth_class = config['authentication']
|
116
|
+
async def handle_authentication(self) -> None:
|
120
117
|
if self.auth:
|
121
|
-
if not
|
118
|
+
if not config.AUTHENTICATION:
|
122
119
|
logger.critical('"AUTHENTICATION" has not been set in configs')
|
123
|
-
raise
|
124
|
-
user =
|
125
|
-
self.request.set_user(user=user)
|
120
|
+
raise APIError
|
121
|
+
self.request.user = await config.AUTHENTICATION.authentication(self.request)
|
126
122
|
|
127
|
-
def handle_throttling(self) -> None:
|
128
|
-
if throttling := self.throttling or config
|
129
|
-
|
130
|
-
|
131
|
-
throttling_key = f'{time}-{key}'
|
132
|
-
if throttling_storage[throttling_key] > throttling.rate:
|
133
|
-
raise ThrottlingException
|
123
|
+
async def handle_throttling(self) -> None:
|
124
|
+
if throttling := self.throttling or config.THROTTLING:
|
125
|
+
if await get_throttling_from_cache(self.request, duration=throttling.duration) + 1 > throttling.rate:
|
126
|
+
raise ThrottlingAPIError
|
134
127
|
|
135
|
-
|
128
|
+
await increment_throttling_in_cache(self.request, duration=throttling.duration)
|
136
129
|
|
137
|
-
def
|
130
|
+
async def handle_permission(self) -> None:
|
138
131
|
for perm in self.permissions:
|
139
132
|
if type(perm.authorization).__name__ != 'method':
|
140
133
|
logger.error(f'{perm.__name__}.authorization should be "classmethod"')
|
141
|
-
|
142
|
-
if perm.authorization(
|
143
|
-
raise
|
134
|
+
raise AuthorizationAPIError
|
135
|
+
if await perm.authorization(self.request) is False:
|
136
|
+
raise AuthorizationAPIError
|
144
137
|
|
145
138
|
def handle_input_validation(self):
|
146
139
|
if self.input_model:
|
147
|
-
validated_data = self.validate_input(model=self.input_model, request=self.request)
|
148
|
-
self.request.set_validated_data(validated_data)
|
140
|
+
self.request.validated_data = self.validate_input(model=self.input_model, request=self.request)
|
149
141
|
|
150
142
|
@classmethod
|
151
143
|
def validate_input(cls, model, request: Request):
|
144
|
+
if isinstance(request.data, bytes):
|
145
|
+
raise BadRequestAPIError(detail='Content-Type is not valid')
|
146
|
+
if request.data is None:
|
147
|
+
raise BadRequestAPIError(detail='Request body is required')
|
152
148
|
try:
|
153
|
-
|
154
|
-
|
155
|
-
return model(**request.data)
|
149
|
+
# `request` will be ignored in regular `BaseModel`
|
150
|
+
return model(**request.data, request=request)
|
156
151
|
except ValidationError as validation_error:
|
157
152
|
error = {'.'.join(loc for loc in e['loc']): e['msg'] for e in validation_error.errors()}
|
158
|
-
raise
|
153
|
+
raise BadRequestAPIError(detail=error)
|
159
154
|
except JSONDecodeError:
|
160
|
-
raise
|
161
|
-
|
162
|
-
@staticmethod
|
163
|
-
def clean_path_variables(func: Callable, request_path_variables: dict):
|
164
|
-
for name, value in request_path_variables.items():
|
165
|
-
for variable_name, variable_type in func.__annotations__.items():
|
166
|
-
if name == variable_name:
|
167
|
-
# Check the type and convert the value
|
168
|
-
if variable_type is bool:
|
169
|
-
request_path_variables[name] = value.lower() not in ['false', '0']
|
170
|
-
|
171
|
-
elif variable_type is int:
|
172
|
-
try:
|
173
|
-
request_path_variables[name] = int(value)
|
174
|
-
except ValueError:
|
175
|
-
raise InvalidPathVariableException(value=value, variable_type=variable_type)
|
176
|
-
break
|
155
|
+
raise JSONDecodeAPIError
|
177
156
|
|
178
157
|
|
179
158
|
class GenericAPI:
|
180
|
-
input_model = None
|
181
|
-
output_model = None
|
159
|
+
input_model: type[ModelSerializer] | type[BaseModel] = None
|
160
|
+
output_model: type[ModelSerializer] | type[BaseModel] = None
|
182
161
|
auth: bool = False
|
183
162
|
permissions: list | None = None
|
184
163
|
throttling: Throttling | None = None
|
@@ -186,40 +165,41 @@ class GenericAPI:
|
|
186
165
|
cache_exp_time: timedelta | int | None = None
|
187
166
|
|
188
167
|
async def get(self, *args, **kwargs):
|
189
|
-
raise
|
168
|
+
raise MethodNotAllowedAPIError
|
190
169
|
|
191
170
|
async def post(self, *args, **kwargs):
|
192
|
-
raise
|
171
|
+
raise MethodNotAllowedAPIError
|
193
172
|
|
194
173
|
async def put(self, *args, **kwargs):
|
195
|
-
raise
|
174
|
+
raise MethodNotAllowedAPIError
|
196
175
|
|
197
176
|
async def patch(self, *args, **kwargs):
|
198
|
-
raise
|
177
|
+
raise MethodNotAllowedAPIError
|
199
178
|
|
200
179
|
async def delete(self, *args, **kwargs):
|
201
|
-
raise
|
180
|
+
raise MethodNotAllowedAPIError
|
202
181
|
|
203
|
-
|
204
|
-
|
205
|
-
match kwargs['request'].method:
|
182
|
+
async def call_method(self, request: Request):
|
183
|
+
match request.method:
|
206
184
|
case 'GET':
|
207
|
-
func =
|
185
|
+
func = self.get
|
208
186
|
case 'POST':
|
209
|
-
func =
|
187
|
+
func = self.post
|
210
188
|
case 'PUT':
|
211
|
-
func =
|
189
|
+
func = self.put
|
212
190
|
case 'PATCH':
|
213
|
-
func =
|
191
|
+
func = self.patch
|
214
192
|
case 'DELETE':
|
215
|
-
func =
|
193
|
+
func = self.delete
|
194
|
+
case _:
|
195
|
+
raise MethodNotAllowedAPIError
|
216
196
|
|
217
197
|
return await API(
|
218
|
-
input_model=
|
219
|
-
output_model=
|
220
|
-
auth=
|
221
|
-
permissions=
|
222
|
-
throttling=
|
223
|
-
cache=
|
224
|
-
cache_exp_time=
|
225
|
-
)(func)(
|
198
|
+
input_model=self.input_model,
|
199
|
+
output_model=self.output_model,
|
200
|
+
auth=self.auth,
|
201
|
+
permissions=self.permissions,
|
202
|
+
throttling=self.throttling,
|
203
|
+
cache=self.cache,
|
204
|
+
cache_exp_time=self.cache_exp_time,
|
205
|
+
)(func)(request=request)
|
panther/authentications.py
CHANGED
@@ -1,18 +1,22 @@
|
|
1
1
|
import logging
|
2
2
|
import time
|
3
3
|
from abc import abstractmethod
|
4
|
+
from datetime import timezone, datetime
|
4
5
|
from typing import Literal
|
5
6
|
|
7
|
+
from panther.base_websocket import Websocket
|
6
8
|
from panther.cli.utils import import_error
|
7
9
|
from panther.configs import config
|
10
|
+
from panther.db.connections import redis
|
8
11
|
from panther.db.models import BaseUser, Model
|
9
|
-
from panther.exceptions import
|
12
|
+
from panther.exceptions import AuthenticationAPIError
|
10
13
|
from panther.request import Request
|
14
|
+
from panther.utils import generate_hash_value_from_string
|
11
15
|
|
12
16
|
try:
|
13
17
|
from jose import JWTError, jwt
|
14
18
|
except ModuleNotFoundError as e:
|
15
|
-
import_error(e, package='python-jose')
|
19
|
+
raise import_error(e, package='python-jose')
|
16
20
|
|
17
21
|
logger = logging.getLogger('panther')
|
18
22
|
|
@@ -20,67 +24,79 @@ logger = logging.getLogger('panther')
|
|
20
24
|
class BaseAuthentication:
|
21
25
|
@classmethod
|
22
26
|
@abstractmethod
|
23
|
-
def authentication(cls, request: Request):
|
27
|
+
async def authentication(cls, request: Request | Websocket):
|
24
28
|
"""Return Instance of User"""
|
25
29
|
msg = f'{cls.__name__}.authentication() is not implemented.'
|
26
30
|
raise cls.exception(msg) from None
|
27
31
|
|
28
32
|
@staticmethod
|
29
|
-
def exception(message: str, /) -> type[
|
33
|
+
def exception(message: str, /) -> type[AuthenticationAPIError]:
|
30
34
|
logger.error(f'Authentication Error: "{message}"')
|
31
|
-
return
|
35
|
+
return AuthenticationAPIError
|
32
36
|
|
33
37
|
|
34
38
|
class JWTAuthentication(BaseAuthentication):
|
35
39
|
model = BaseUser
|
36
40
|
keyword = 'Bearer'
|
37
41
|
algorithm = 'HS256'
|
38
|
-
HTTP_HEADER_ENCODING = 'iso-8859-1' #
|
42
|
+
HTTP_HEADER_ENCODING = 'iso-8859-1' # RFC5987
|
39
43
|
|
40
44
|
@classmethod
|
41
|
-
def get_authorization_header(cls, request: Request) ->
|
42
|
-
auth
|
45
|
+
def get_authorization_header(cls, request: Request | Websocket) -> str:
|
46
|
+
if auth := request.headers.authorization:
|
47
|
+
return auth
|
48
|
+
msg = 'Authorization is required'
|
49
|
+
raise cls.exception(msg) from None
|
50
|
+
|
51
|
+
@classmethod
|
52
|
+
async def authentication(cls, request: Request | Websocket) -> Model:
|
53
|
+
auth_header = cls.get_authorization_header(request).split()
|
43
54
|
|
44
|
-
if
|
45
|
-
msg = 'Authorization
|
55
|
+
if len(auth_header) != 2:
|
56
|
+
msg = 'Authorization should have 2 part'
|
46
57
|
raise cls.exception(msg) from None
|
47
58
|
|
48
|
-
|
49
|
-
try:
|
50
|
-
auth = auth.encode(JWTAuthentication.HTTP_HEADER_ENCODING)
|
51
|
-
except UnicodeEncodeError as e:
|
52
|
-
raise cls.exception(e) from None
|
59
|
+
bearer, token = auth_header
|
53
60
|
|
54
|
-
|
61
|
+
try:
|
62
|
+
token.encode(JWTAuthentication.HTTP_HEADER_ENCODING)
|
63
|
+
except UnicodeEncodeError as e:
|
64
|
+
raise cls.exception(e) from None
|
55
65
|
|
56
|
-
|
57
|
-
def authentication(cls, request: Request) -> Model:
|
58
|
-
auth = cls.get_authorization_header(request).split()
|
59
|
-
if not auth or auth[0].lower() != cls.keyword.lower().encode():
|
66
|
+
if bearer.lower() != cls.keyword.lower():
|
60
67
|
msg = 'Authorization keyword is not valid'
|
61
68
|
raise cls.exception(msg) from None
|
62
|
-
if len(auth) != 2:
|
63
|
-
msg = 'Authorization should have 2 part'
|
64
|
-
raise cls.exception(msg) from None
|
65
69
|
|
66
|
-
|
67
|
-
|
68
|
-
except UnicodeError:
|
69
|
-
msg = 'Unicode Error'
|
70
|
+
if redis.is_connected and await cls._check_in_cache(token=token):
|
71
|
+
msg = 'User logged out'
|
70
72
|
raise cls.exception(msg) from None
|
71
73
|
|
72
74
|
payload = cls.decode_jwt(token)
|
73
|
-
|
75
|
+
user = await cls.get_user(payload)
|
76
|
+
user._auth_token = token
|
77
|
+
return user
|
74
78
|
|
75
79
|
@classmethod
|
76
|
-
def
|
80
|
+
def decode_jwt(cls, token: str) -> dict:
|
81
|
+
"""Decode JWT token to user_id (it can return multiple variable ... )"""
|
82
|
+
try:
|
83
|
+
return jwt.decode(
|
84
|
+
token=token,
|
85
|
+
key=config.JWT_CONFIG.key,
|
86
|
+
algorithms=[config.JWT_CONFIG.algorithm],
|
87
|
+
)
|
88
|
+
except JWTError as e:
|
89
|
+
raise cls.exception(e) from None
|
90
|
+
|
91
|
+
@classmethod
|
92
|
+
async def get_user(cls, payload: dict) -> Model:
|
77
93
|
"""Get UserModel from config, else use default UserModel from cls.model"""
|
78
94
|
if (user_id := payload.get('user_id')) is None:
|
79
|
-
msg = 'Payload does not have user_id'
|
95
|
+
msg = 'Payload does not have `user_id`'
|
80
96
|
raise cls.exception(msg)
|
81
97
|
|
82
|
-
user_model = config
|
83
|
-
if user := user_model.find_one(id=user_id):
|
98
|
+
user_model = config.USER_MODEL or cls.model
|
99
|
+
if user := await user_model.find_one(id=user_id):
|
84
100
|
return user
|
85
101
|
|
86
102
|
msg = 'User not found'
|
@@ -89,11 +105,11 @@ class JWTAuthentication(BaseAuthentication):
|
|
89
105
|
@classmethod
|
90
106
|
def encode_jwt(cls, user_id: str, token_type: Literal['access', 'refresh'] = 'access') -> str:
|
91
107
|
"""Encode JWT from user_id."""
|
92
|
-
issued_at =
|
108
|
+
issued_at = datetime.now(timezone.utc).timestamp()
|
93
109
|
if token_type == 'access':
|
94
|
-
expire = issued_at + config
|
110
|
+
expire = issued_at + config.JWT_CONFIG.life_time
|
95
111
|
else:
|
96
|
-
expire = issued_at + config
|
112
|
+
expire = issued_at + config.JWT_CONFIG.refresh_life_time
|
97
113
|
|
98
114
|
claims = {
|
99
115
|
'token_type': token_type,
|
@@ -103,33 +119,48 @@ class JWTAuthentication(BaseAuthentication):
|
|
103
119
|
}
|
104
120
|
return jwt.encode(
|
105
121
|
claims,
|
106
|
-
key=config
|
107
|
-
algorithm=config
|
122
|
+
key=config.JWT_CONFIG.key,
|
123
|
+
algorithm=config.JWT_CONFIG.algorithm,
|
108
124
|
)
|
109
125
|
|
110
126
|
@classmethod
|
111
|
-
def
|
112
|
-
"""
|
113
|
-
return
|
127
|
+
def login(cls, user_id: str) -> dict:
|
128
|
+
"""Return dict of access and refresh token"""
|
129
|
+
return {
|
130
|
+
'access_token': cls.encode_jwt(user_id=user_id),
|
131
|
+
'refresh_token': cls.encode_jwt(user_id=user_id, token_type='refresh')
|
132
|
+
}
|
114
133
|
|
115
134
|
@classmethod
|
116
|
-
def
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
)
|
124
|
-
|
125
|
-
|
135
|
+
async def logout(cls, raw_token: str) -> None:
|
136
|
+
*_, token = raw_token.split()
|
137
|
+
if redis.is_connected:
|
138
|
+
payload = cls.decode_jwt(token=token)
|
139
|
+
remaining_exp_time = payload['exp'] - time.time()
|
140
|
+
await cls._set_in_cache(token=token, exp=int(remaining_exp_time))
|
141
|
+
else:
|
142
|
+
logger.error('`redis` middleware is required for `logout()`')
|
143
|
+
|
144
|
+
@classmethod
|
145
|
+
async def _set_in_cache(cls, token: str, exp: int) -> None:
|
146
|
+
key = generate_hash_value_from_string(token)
|
147
|
+
await redis.set(key, b'', ex=exp)
|
126
148
|
|
127
149
|
@classmethod
|
128
|
-
def
|
129
|
-
|
130
|
-
return
|
150
|
+
async def _check_in_cache(cls, token: str) -> bool:
|
151
|
+
key = generate_hash_value_from_string(token)
|
152
|
+
return bool(await redis.exists(key))
|
131
153
|
|
132
154
|
@staticmethod
|
133
|
-
def exception(message: str | JWTError | UnicodeEncodeError, /) -> type[
|
155
|
+
def exception(message: str | JWTError | UnicodeEncodeError, /) -> type[AuthenticationAPIError]:
|
134
156
|
logger.error(f'JWT Authentication Error: "{message}"')
|
135
|
-
return
|
157
|
+
return AuthenticationAPIError
|
158
|
+
|
159
|
+
|
160
|
+
class QueryParamJWTAuthentication(JWTAuthentication):
|
161
|
+
@classmethod
|
162
|
+
def get_authorization_header(cls, request: Request | Websocket) -> str:
|
163
|
+
if auth := request.query_params.get('authorization'):
|
164
|
+
return auth
|
165
|
+
msg = 'Authorization is required'
|
166
|
+
raise cls.exception(msg) from None
|
panther/background_tasks.py
CHANGED
@@ -9,7 +9,6 @@ from typing import Callable, Literal
|
|
9
9
|
from panther._utils import is_function_async
|
10
10
|
from panther.utils import Singleton
|
11
11
|
|
12
|
-
|
13
12
|
__all__ = (
|
14
13
|
'BackgroundTask',
|
15
14
|
'background_tasks',
|
@@ -40,6 +39,7 @@ class BackgroundTask:
|
|
40
39
|
self._last_run: datetime.datetime | None = None
|
41
40
|
self._timedelta: datetime.timedelta = datetime.timedelta(minutes=1)
|
42
41
|
self._time: datetime.time | None = None
|
42
|
+
self._day_of_week: int | None = None
|
43
43
|
self._unit: Literal['seconds', 'minutes', 'hours', 'days', 'weeks'] | None = None
|
44
44
|
|
45
45
|
def interval(self, interval: int, /) -> Self:
|
@@ -89,18 +89,24 @@ class BackgroundTask:
|
|
89
89
|
self._timedelta = datetime.timedelta(weeks=weeks)
|
90
90
|
return self
|
91
91
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
92
|
+
def on(
|
93
|
+
self,
|
94
|
+
day_of_week: Literal['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'],
|
95
|
+
/
|
96
|
+
) -> Self:
|
97
|
+
"""
|
98
|
+
Set day to schedule the task, useful on `.every_weeks()`
|
99
|
+
"""
|
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()`')
|
109
|
+
return self
|
104
110
|
|
105
111
|
def at(self, _time: datetime.time, /) -> Self:
|
106
112
|
"""
|
@@ -133,6 +139,11 @@ class BackgroundTask:
|
|
133
139
|
if self._last_run and (self._last_run + self._timedelta) > now:
|
134
140
|
return True
|
135
141
|
|
142
|
+
# Check day of week
|
143
|
+
if self._day_of_week is not None:
|
144
|
+
if self._day_of_week != now.weekday():
|
145
|
+
return True
|
146
|
+
|
136
147
|
# We don't have time condition, so run
|
137
148
|
if self._time is None:
|
138
149
|
self._last_run = now
|
@@ -188,7 +199,7 @@ class BackgroundTasks(Singleton):
|
|
188
199
|
|
189
200
|
def add_task(self, task: BackgroundTask):
|
190
201
|
if self._initialized is False:
|
191
|
-
logger.error('Task will be ignored, `BACKGROUND_TASKS` is not True in `
|
202
|
+
logger.error('Task will be ignored, `BACKGROUND_TASKS` is not True in `configs`')
|
192
203
|
return
|
193
204
|
|
194
205
|
if not self._is_instance_of_task(task):
|