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.
Files changed (56) 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 +18 -24
  18. panther/db/cursor.py +0 -1
  19. panther/db/models.py +24 -8
  20. panther/db/queries/base_queries.py +2 -5
  21. panther/db/queries/mongodb_queries.py +17 -20
  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 +11 -8
  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 +53 -47
  44. panther/routings.py +12 -12
  45. panther/serializer.py +19 -20
  46. panther/test.py +73 -58
  47. panther/throttling.py +68 -3
  48. panther/utils.py +5 -11
  49. {panther-5.0.0b3.dist-info → panther-5.0.0b4.dist-info}/METADATA +1 -1
  50. panther-5.0.0b4.dist-info/RECORD +75 -0
  51. panther/monitoring.py +0 -34
  52. panther-5.0.0b3.dist-info/RECORD +0 -75
  53. {panther-5.0.0b3.dist-info → panther-5.0.0b4.dist-info}/WHEEL +0 -0
  54. {panther-5.0.0b3.dist-info → panther-5.0.0b4.dist-info}/entry_points.txt +0 -0
  55. {panther-5.0.0b3.dist-info → panther-5.0.0b4.dist-info}/licenses/LICENSE +0 -0
  56. {panther-5.0.0b3.dist-info → panther-5.0.0b4.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  import logging
2
2
  import time
3
3
  from abc import abstractmethod
4
- from datetime import timezone, datetime
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
- model = BaseUser
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
- auth_header = cls.get_authorization_header(request).split()
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 should have 2 part'
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
- if redis.is_connected and await cls._check_in_cache(token=token):
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 to user_id (it can return multiple variable ... )"""
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
- """Get UserModel from config, else use default UserModel from cls.model"""
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 = config.USER_MODEL or cls.model
99
- if user := await user_model.find_one(id=user_id):
100
- return user
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
- msg = 'User not found'
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
- """Encode JWT from user_id."""
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, user_id: str) -> dict:
128
- """Return dict of access and refresh token"""
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=user_id),
131
- 'refresh_token': cls.encode_jwt(user_id=user_id, token_type='refresh')
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, 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))
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
- logger.error('`redis` middleware is required for `logout()`')
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 _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)
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 _check_in_cache(cls, token: str) -> bool:
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
- async def authentication(cls, request: Request | Websocket) -> Model:
174
- token = cls.get_authorization_header(request)
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
@@ -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: