panther 1.7.9__py3-none-any.whl → 1.7.10__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 CHANGED
@@ -1,6 +1,6 @@
1
1
  from .main import Panther
2
2
 
3
- __version__ = '1.7.9'
3
+ __version__ = '1.7.10'
4
4
 
5
5
 
6
6
  def version():
panther/_utils.py CHANGED
@@ -102,13 +102,3 @@ def read_multipart_form_data(content_type: str, body: str) -> dict:
102
102
  # fields[field_name] = data
103
103
  logger.error("We Don't Handle Files In Multipart Request Yet.")
104
104
  return fields
105
-
106
-
107
- def collect_path_variables(request_path: str, found_path: str) -> dict:
108
- found_path = found_path.removesuffix('/').removeprefix('/')
109
- request_path = request_path.removesuffix('/').removeprefix('/')
110
- path_variables = dict()
111
- for f_path, r_path in zip(found_path.split('/'), request_path.split('/')):
112
- if f_path.startswith('<'):
113
- path_variables[f_path[1:-1]] = r_path
114
- return path_variables
@@ -1,3 +1,4 @@
1
+ from abc import abstractmethod
1
2
  from datetime import datetime
2
3
  from panther.logger import logger
3
4
  from panther.configs import config
@@ -12,7 +13,22 @@ except ImportError:
12
13
  exit()
13
14
 
14
15
 
15
- class JWTAuthentication:
16
+ class BaseAuthentication:
17
+ @classmethod
18
+ @abstractmethod
19
+ def authentication(cls, request: Request):
20
+ """
21
+ Return User Instance
22
+ """
23
+ raise cls.exception(f'{cls.__name__}.authentication() is not implemented.')
24
+
25
+ @staticmethod
26
+ def exception(message: str, /):
27
+ logger.error(f'Authentication Error: "{message}"')
28
+ return AuthenticationException
29
+
30
+
31
+ class JWTAuthentication(BaseAuthentication):
16
32
  model = BaseUser
17
33
  keyword = 'Bearer'
18
34
  algorithm = 'HS256'
panther/caching.py CHANGED
@@ -12,7 +12,7 @@ from panther.response import Response, ResponseDataTypes
12
12
 
13
13
 
14
14
  caches = dict()
15
- Cached = namedtuple('Cached', ['data', 'status_code'])
15
+ CachedResponse = namedtuple('Cached', ['data', 'status_code'])
16
16
 
17
17
 
18
18
  def cache_key(request: Request, /):
@@ -20,7 +20,7 @@ def cache_key(request: Request, /):
20
20
  return f'{client}-{request.path}-{request.data}'
21
21
 
22
22
 
23
- def get_cached_response_data(*, request: Request) -> Cached | None:
23
+ def get_cached_response_data(*, request: Request) -> CachedResponse | None:
24
24
  """
25
25
  If redis.is_connected:
26
26
  Get Cached Data From Redis
@@ -31,14 +31,14 @@ def get_cached_response_data(*, request: Request) -> Cached | None:
31
31
  if redis.is_connected: # NOQA: Unresolved References
32
32
  data = (redis.get(key) or b'{}').decode()
33
33
  if cached := json.loads(data):
34
- return Cached(*cached)
34
+ return CachedResponse(*cached)
35
35
  else:
36
36
  return None
37
37
 
38
38
  else:
39
39
  global caches
40
40
  if cached := caches.get(key):
41
- return Cached(*cached)
41
+ return CachedResponse(*cached)
42
42
  else:
43
43
  return None
44
44
 
panther/configs.py CHANGED
@@ -18,32 +18,32 @@ class Config(TypedDict):
18
18
  base_dir: Path
19
19
  monitoring: bool
20
20
  log_queries: bool
21
- urls: dict
22
- middlewares: list
23
- reversed_middlewares: list
24
- db_engine: str
25
21
  default_cache_exp: timedelta | None
22
+ throttling: Throttling | None
26
23
  secret_key: bytes | None
24
+ middlewares: list
25
+ reversed_middlewares: list
26
+ user_model: ModelMetaclass | None
27
27
  authentication: ModelMetaclass | None
28
28
  jwt_config: JWTConfig | None
29
- user_model: ModelMetaclass | None
30
- throttling: Throttling | None
31
29
  models: list[dict]
30
+ urls: dict
31
+ db_engine: str
32
32
 
33
33
 
34
34
  config: Config = {
35
35
  'base_dir': Path(),
36
36
  'monitoring': False,
37
37
  'log_queries': False,
38
+ 'default_cache_exp': None,
39
+ 'throttling': None,
38
40
  'secret_key': None,
39
- 'urls': {},
40
41
  'middlewares': [],
41
42
  'reversed_middlewares': [],
42
- 'db_engine': '',
43
- 'default_cache_exp': None,
44
- 'jwt_config': None,
45
- 'authentication': None,
46
43
  'user_model': None,
47
- 'throttling': None,
44
+ 'authentication': None,
45
+ 'jwt_config': None,
48
46
  'models': [],
47
+ 'urls': {},
48
+ 'db_engine': '', # TODO: Should we set default db_engine=pantherdb ?
49
49
  }
panther/db/models.py CHANGED
@@ -1,35 +1,29 @@
1
1
  import bson
2
- from pydantic import Field, BaseModel as PydanticBaseModel
2
+ from pydantic import field_validator, Field, BaseModel as PydanticBaseModel
3
3
 
4
4
  from panther.configs import config
5
5
  from panther.db.queries import Query
6
6
 
7
7
 
8
- class BsonObjectId(bson.ObjectId):
9
- @classmethod
10
- def __get_validators__(cls):
11
- yield cls.validate
12
-
13
- @classmethod
14
- def validate(cls, v):
15
- if isinstance(v, str):
16
- try:
17
- bson.ObjectId(v)
18
- except Exception:
19
- raise TypeError('Invalid ObjectId')
20
- elif not isinstance(v, bson.ObjectId):
21
- raise TypeError('ObjectId required')
22
- return str(v)
23
-
24
-
25
8
  if config['db_engine'] == 'pantherdb':
26
9
  IDType = int
27
10
  else:
28
- IDType = BsonObjectId
11
+ IDType = str
29
12
 
30
13
 
31
14
  class Model(PydanticBaseModel, Query):
32
- id: IDType | None = Field(alias='_id')
15
+ id: IDType | None = Field(validation_alias='_id')
16
+
17
+ @field_validator('id', mode='before')
18
+ def validate_id(cls, value):
19
+ if isinstance(value, str):
20
+ try:
21
+ bson.ObjectId(value)
22
+ except Exception:
23
+ raise ValueError('Invalid ObjectId')
24
+ elif not isinstance(value, bson.ObjectId):
25
+ raise ValueError('ObjectId required')
26
+ return str(value)
33
27
 
34
28
  @property
35
29
  def _id(self):
@@ -1,10 +1,17 @@
1
- from typing import Self
1
+ import sys
2
2
 
3
3
  from panther.db.connection import db # NOQA: F401
4
4
  from panther.exceptions import DBException
5
5
  from panther.db.utils import clean_object_id_in_dicts, merge_dicts
6
6
 
7
7
 
8
+ if sys.version_info.minor >= 11:
9
+ from typing import Self
10
+ else:
11
+ from typing import TypeVar
12
+ Self = TypeVar("Self", bound="BaseMongoDBQuery")
13
+
14
+
8
15
  class BaseMongoDBQuery:
9
16
 
10
17
  @classmethod
@@ -65,16 +72,16 @@ class BaseMongoDBQuery:
65
72
  @classmethod
66
73
  def update_one(cls, _filter, _data: dict = None, /, **kwargs) -> bool:
67
74
  clean_object_id_in_dicts(_filter)
68
- _update = {'$set': kwargs | {}}
69
- if isinstance(_data, dict):
70
- _data['$set'] = _data.get('$set', {}) | (kwargs or {})
75
+ _update = {'$set': cls._merge(_data, kwargs)}
71
76
 
72
- result = eval(f'db.session.{cls.__name__}.update_one(_filter, _data | _update)')
77
+ result = eval(f'db.session.{cls.__name__}.update_one(_filter, _update)')
73
78
  return bool(result.updated_count)
74
79
 
75
80
  @classmethod
76
81
  def update_many(cls, _filter, _data: dict = None, /, **kwargs) -> int:
82
+ clean_object_id_in_dicts(_filter)
77
83
  _update = {'$set': cls._merge(_data, kwargs)}
84
+
78
85
  result = eval(f'db.session.{cls.__name__}.update_many(_filter, _update)')
79
86
  return result.updated_count
80
87
 
@@ -1,10 +1,17 @@
1
- from typing import Self
1
+ import sys
2
2
 
3
3
  from panther.db.connection import db
4
- from panther.db.utils import merge_dicts
4
+ from panther.db.utils import merge_dicts, clean_object_id_in_dicts
5
5
  from panther.exceptions import DBException
6
6
 
7
7
 
8
+ if sys.version_info.minor >= 11:
9
+ from typing import Self
10
+ else:
11
+ from typing import TypeVar
12
+ Self = TypeVar("Self", bound="BasePantherDBQuery")
13
+
14
+
8
15
  class BasePantherDBQuery:
9
16
 
10
17
  @classmethod
@@ -53,10 +60,12 @@ class BasePantherDBQuery:
53
60
 
54
61
  @classmethod
55
62
  def update_one(cls, _filter, _data: dict = None, /, **kwargs) -> bool:
63
+ clean_object_id_in_dicts(_filter)
56
64
  return db.session.collection(cls.__name__).update_one(_filter, **cls._merge(_data, kwargs))
57
65
 
58
66
  @classmethod
59
67
  def update_many(cls, _filter, _data: dict = None, /, **kwargs) -> int:
68
+ clean_object_id_in_dicts(_filter)
60
69
  return db.session.collection(cls.__name__).update_many(_filter, **cls._merge(_data, kwargs))
61
70
 
62
71
  # # # # # Other # # # # #
@@ -1,4 +1,5 @@
1
- from typing import Self, NoReturn
1
+ import sys
2
+ from typing import NoReturn
2
3
 
3
4
  from pydantic import ValidationError
4
5
 
@@ -18,6 +19,13 @@ __all__ = (
18
19
  )
19
20
 
20
21
 
22
+ if sys.version_info.minor >= 11:
23
+ from typing import Self
24
+ else:
25
+ from typing import TypeVar
26
+ Self = TypeVar("Self", bound="Query")
27
+
28
+
21
29
  class Query(BaseQuery):
22
30
 
23
31
  @classmethod
@@ -71,7 +79,7 @@ class Query(BaseQuery):
71
79
 
72
80
  @classmethod
73
81
  @log_query
74
- def insert_many(cls, _data: dict = None, **kwargs):
82
+ def insert_many(cls, _data: dict = None, /, **kwargs):
75
83
  return super().insert_many(_data, **kwargs)
76
84
 
77
85
  # # # # # Delete # # # # #
@@ -87,23 +95,23 @@ class Query(BaseQuery):
87
95
 
88
96
  @classmethod
89
97
  @log_query
90
- def delete_one(cls, **kwargs) -> bool:
98
+ def delete_one(cls, _data: dict = None, /, **kwargs) -> bool:
91
99
  """
92
100
  example:
93
101
  >>> from example.app.models import User
94
102
  >>> User.delete_one(id=1)
95
103
  """
96
- return super().delete_one(**kwargs)
104
+ return super().delete_one(_data, **kwargs)
97
105
 
98
106
  @classmethod
99
107
  @log_query
100
- def delete_many(cls, **kwargs) -> int:
108
+ def delete_many(cls, _data: dict = None, /, **kwargs) -> int:
101
109
  """
102
110
  example:
103
111
  >>> from example.app.models import User
104
112
  >>> User.delete_many(last_name='Rn')
105
113
  """
106
- return super().delete_many(**kwargs)
114
+ return super().delete_many(_data, **kwargs)
107
115
 
108
116
  # # # # # Update # # # # #
109
117
  @log_query
@@ -171,3 +179,14 @@ class Query(BaseQuery):
171
179
  return False, obj
172
180
  else:
173
181
  return True, cls.insert_one(**kwargs)
182
+
183
+ @log_query
184
+ def save(self, **kwargs) -> None:
185
+ """
186
+ example:
187
+ >>> from example.app.models import User
188
+ >>> user = User.find_one(name='Ali')
189
+ >>> user.name = 'Saba'
190
+ >>> user.save()
191
+ """
192
+ raise DBException('save() is not supported yes')
panther/logger.py CHANGED
@@ -7,8 +7,6 @@ from logging.config import dictConfig
7
7
 
8
8
 
9
9
  LOGS_DIR = config['base_dir'] / 'logs'
10
- if not os.path.exists(LOGS_DIR):
11
- os.makedirs(LOGS_DIR)
12
10
 
13
11
 
14
12
  class LogConfig(BaseModel):
@@ -79,7 +77,14 @@ class LogConfig(BaseModel):
79
77
  }
80
78
 
81
79
 
82
- dictConfig(LogConfig().model_dump())
80
+ try:
81
+ dictConfig(LogConfig().model_dump())
82
+ except ValueError:
83
+ LOGS_DIR = config['base_dir'] / 'logs'
84
+ if not os.path.exists(LOGS_DIR):
85
+ os.makedirs(LOGS_DIR)
86
+
87
+
83
88
  logger = logging.getLogger('panther')
84
89
  query_logger = logging.getLogger('query')
85
90
  monitoring_logger = logging.getLogger('monitoring')
panther/main.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import os
2
2
  import ast
3
+ import sys
3
4
  import asyncio
4
5
  from pathlib import Path
5
6
  from runpy import run_path
@@ -12,8 +13,8 @@ from panther.exceptions import APIException
12
13
  from panther.configs import JWTConfig, config
13
14
  from panther.middlewares.base import BaseMiddleware
14
15
  from panther.middlewares.monitoring import Middleware as MonitoringMiddleware
15
- from panther.routings import find_endpoint, check_urls, collect_urls, finalize_urls
16
- from panther._utils import http_response, import_class, read_body, collect_path_variables
16
+ from panther.routings import find_endpoint, check_and_load_urls, finalize_urls, flatten_urls, collect_path_variables
17
+ from panther._utils import http_response, import_class, read_body
17
18
 
18
19
  """ We can't import logger on the top cause it needs config['base_dir'] ans its fill in __init__ """
19
20
 
@@ -21,9 +22,12 @@ from panther._utils import http_response, import_class, read_body, collect_path_
21
22
  class Panther:
22
23
 
23
24
  def __init__(self, name):
25
+ from panther.logger import logger
24
26
  os.system('clear')
25
27
  config['base_dir'] = Path(name).resolve().parent
26
28
  self.panther_dir = Path(__file__).parent
29
+ if sys.version_info.minor < 11:
30
+ logger.warning('Use Python Version 3.11+ For Better Performance.')
27
31
  self.load_configs()
28
32
 
29
33
  def load_configs(self) -> None:
@@ -48,7 +52,7 @@ class Panther:
48
52
  config['jwt_config'] = self._get_jwt_config()
49
53
 
50
54
  # Find Database Models
51
- self.collect_models_for_panel()
55
+ self._collect_models()
52
56
 
53
57
  # Check & Collect URLs
54
58
  # check_urls should be the last call in load_configs,
@@ -63,11 +67,6 @@ class Panther:
63
67
  if config['monitoring']:
64
68
  logger.info('Run "panther monitor" in another session for Monitoring.')
65
69
 
66
- def _load_urls(self) -> dict:
67
- urls = check_urls(self.settings.get('URLs')) or {}
68
- collect_urls('', urls, collected_urls := dict())
69
- return finalize_urls(collected_urls)
70
-
71
70
  def _check_configs(self):
72
71
  from panther.logger import logger
73
72
  """Read the config file and put it as dict in self.settings"""
@@ -76,6 +75,7 @@ class Panther:
76
75
  self.settings = run_path(str(configs_path))
77
76
  except FileNotFoundError:
78
77
  logger.critical('core/configs.py Not Found.')
78
+ # TODO: Exit() Here
79
79
 
80
80
  def _get_secret_key(self) -> bytes | None:
81
81
  if secret_key := self.settings.get('SECRET_KEY'):
@@ -99,19 +99,21 @@ class Panther:
99
99
  middlewares.append(Middleware(**data)) # NOQA: Py Argument List
100
100
  return middlewares
101
101
 
102
- def _get_authentication_class(self) -> ModelMetaclass | None:
103
- return self.settings.get('AUTHENTICATION') and import_class(self.settings['AUTHENTICATION'])
104
-
105
102
  def _get_user_model(self) -> ModelMetaclass:
106
103
  return import_class(self.settings.get('USER_MODEL', 'panther.db.models.BaseUser'))
107
104
 
105
+ def _get_authentication_class(self) -> ModelMetaclass | None:
106
+ return self.settings.get('AUTHENTICATION') and import_class(self.settings['AUTHENTICATION'])
107
+
108
108
  def _get_jwt_config(self) -> JWTConfig:
109
109
  """Only Collect JWT Config If Authentication Is JWTAuthentication"""
110
110
  if getattr(config['authentication'], '__name__', None) == 'JWTAuthentication':
111
111
  user_config = self.settings.get('JWTConfig')
112
112
  return JWTConfig(**user_config) if user_config else JWTConfig(key=config['secret_key'].decode())
113
113
 
114
- def collect_models_for_panel(self):
114
+ @classmethod
115
+ def _collect_models(cls):
116
+ """Collecting models for panel APIs"""
115
117
  from panther.db.models import Model
116
118
 
117
119
  for root, _, files in os.walk(config['base_dir']):
@@ -145,9 +147,14 @@ class Panther:
145
147
  'app': class_path.split('.'),
146
148
  })
147
149
 
150
+ def _load_urls(self) -> dict:
151
+ urls = check_and_load_urls(self.settings.get('URLs')) or {}
152
+ collected_urls = flatten_urls(urls)
153
+ return finalize_urls(collected_urls)
154
+
148
155
  async def __call__(self, scope, receive, send) -> None:
149
156
  """
150
- We Used Python3.11 For asyncio.TaskGroup()
157
+ We Used Python3.11+ For asyncio.TaskGroup()
151
158
  1.
152
159
  async with asyncio.TaskGroup() as tg:
153
160
  tg.create_task(self.run(scope, receive, send))
@@ -161,8 +168,11 @@ class Panther:
161
168
  with ProcessPoolExecutor() as e:
162
169
  e.submit(self.run, scope, receive, send)
163
170
  """
164
- async with asyncio.TaskGroup() as tg:
165
- tg.create_task(self.run(scope, receive, send))
171
+ if sys.version_info.minor >= 11:
172
+ async with asyncio.TaskGroup() as tg:
173
+ tg.create_task(self.run(scope, receive, send))
174
+ else:
175
+ await self.run(scope, receive, send)
166
176
 
167
177
  async def run(self, scope, receive, send):
168
178
  from panther.logger import logger
panther/permissions.py CHANGED
@@ -2,15 +2,13 @@ from panther.request import Request
2
2
 
3
3
 
4
4
  class BasePermission:
5
- """
6
- Just for demonstration
7
- """
5
+
8
6
  @classmethod
9
7
  def authorization(cls, request: Request) -> bool:
10
8
  return True
11
9
 
12
10
 
13
- class AdminPermission:
11
+ class AdminPermission(BasePermission):
14
12
 
15
13
  @classmethod
16
14
  def authorization(cls, request: Request) -> bool:
panther/request.py CHANGED
@@ -96,13 +96,6 @@ class Request:
96
96
  def scheme(self) -> str:
97
97
  return self.scope['scheme']
98
98
 
99
- @property
100
- def data(self):
101
- """Return The Validated Data
102
- It has been set on API.validate_input() while request is happening
103
- """
104
- return self._validated_data
105
-
106
99
  @property
107
100
  def pure_data(self) -> dict:
108
101
  """This is the data before validation"""
@@ -123,6 +116,13 @@ class Request:
123
116
 
124
117
  return self._data
125
118
 
119
+ @property
120
+ def data(self):
121
+ """Return The Validated Data
122
+ It has been set on API.validate_input() while request is happening
123
+ """
124
+ return self._validated_data
125
+
126
126
  def set_validated_data(self, validated_data) -> None:
127
127
  self._validated_data = validated_data
128
128
 
panther/routings.py CHANGED
@@ -11,7 +11,7 @@ from functools import reduce, partial
11
11
  from panther.configs import config
12
12
 
13
13
 
14
- def check_urls(urls: str | None) -> dict | None:
14
+ def check_and_load_urls(urls: str | None) -> dict | None:
15
15
  from panther.logger import logger
16
16
 
17
17
  if urls is None:
@@ -29,30 +29,78 @@ def check_urls(urls: str | None) -> dict | None:
29
29
  return urls_dict
30
30
 
31
31
 
32
- def collect_urls(pre_url: str, urls: dict, final: dict):
32
+ def flatten_urls(urls: dict) -> dict:
33
+ return {k: v for k, v in _flattening_urls(urls)}
34
+
35
+
36
+ def _flattening_urls(data: dict | Callable, url: str = ''):
37
+ # Add `/` add the end of url
38
+ if not url.endswith('/'):
39
+ url = f'{url}/'
40
+
41
+ if isinstance(data, dict):
42
+ for k, v in data.items():
43
+ yield from _flattening_urls(v, f'{url}{k}')
44
+ else:
45
+ # Remove `/` prefix of url
46
+ url = url.removeprefix('/')
47
+
48
+ # Collect it, if it doesn't have problem
49
+ if _is_url_endpoint_valid(url=url, endpoint=data):
50
+ yield url, data
51
+
52
+
53
+ def _is_url_endpoint_valid(url: str, endpoint: Callable) -> bool:
33
54
  from panther.logger import logger
34
55
 
56
+ if endpoint is ...:
57
+ logger.error(f"URL Can't Point To Ellipsis. ('{url}' -> ...)")
58
+ elif endpoint is None:
59
+ logger.error(f"URL Can't Point To None. ('{url}' -> None)")
60
+ elif url and not re.match(r'^[a-zA-Z<>0-9_/-]+$', url):
61
+ logger.error(f"URL Is Not Valid. --> '{url}'")
62
+ else:
63
+ return True
64
+ return False
65
+
66
+
67
+ def finalize_urls(urls: dict) -> dict:
68
+ """convert flat dict to nested"""
69
+ urls_list = list()
35
70
  for url, endpoint in urls.items():
36
- if endpoint is ...:
37
- logger.error(f"URL Can't Point To Ellipsis. ('{pre_url}{url}' -> ...)")
38
- elif endpoint is None:
39
- logger.error(f"URL Can't Point To None. ('{pre_url}{url}' -> None)")
40
- elif url and not re.match(r'[a-zA-Z<>0-9_/-]', url):
41
- logger.error(f"URL Is Not Valid. --> '{pre_url}{url}'")
42
- else:
43
- if not url.endswith('/'):
44
- url = f'{url}/'
45
- if isinstance(endpoint, dict):
46
- if url != '/':
47
- if pre_url:
48
- pre_url = f'{pre_url}/{url}'
49
- else:
50
- pre_url = url
51
- collect_urls(pre_url, endpoint, final)
71
+ path = dict()
72
+ if url == '':
73
+ # This condition only happen when
74
+ # user defines the root url == '' instead of '/'
75
+ url = '/'
76
+
77
+ for single_path in url.split('/')[:-1][::-1]:
78
+ path = {single_path: path or endpoint}
79
+ urls_list.append(path)
80
+ return _merge(*urls_list) if urls_list else {}
81
+
82
+
83
+ def _merge(destination: MutableMapping, *sources) -> MutableMapping:
84
+ """Credit to Travis Clarke --> https://github.com/clarketm/mergedeep"""
85
+ return reduce(partial(_deepmerge), sources, destination)
86
+
87
+
88
+ def _deepmerge(dst, src):
89
+ for key in src:
90
+ if key in dst:
91
+ if _is_recursive_merge(dst[key], src[key]):
92
+ _deepmerge(dst[key], src[key])
52
93
  else:
53
- final[f'{pre_url}{url}'] = endpoint
94
+ dst[key] = deepcopy(src[key])
95
+ else:
96
+ dst[key] = deepcopy(src[key])
97
+ return dst
54
98
 
55
- return urls
99
+
100
+ def _is_recursive_merge(a, b):
101
+ both_mapping = isinstance(a, Mapping) and isinstance(b, Mapping)
102
+ both_counter = isinstance(a, Counter) and isinstance(b, Counter)
103
+ return both_mapping and not both_counter
56
104
 
57
105
 
58
106
  def find_endpoint(path: str) -> tuple[Callable | None, str]:
@@ -61,8 +109,8 @@ def find_endpoint(path: str) -> tuple[Callable | None, str]:
61
109
  path = path.removesuffix('/').removeprefix('/') # 'user/list'
62
110
  paths = path.split('/') # ['user', 'list']
63
111
  paths_len = len(paths)
64
- sub = config['urls']
65
- # sub = {
112
+ urls = config['urls']
113
+ # urls = {
66
114
  # 'user': {
67
115
  # '<id>': <function users at 0x7f579d060220>,
68
116
  # '': <function single_user at 0x7f579d060e00>
@@ -71,7 +119,7 @@ def find_endpoint(path: str) -> tuple[Callable | None, str]:
71
119
  found_path = ''
72
120
  for i, split_path in enumerate(paths):
73
121
  last_path = bool((i + 1) == paths_len)
74
- found = sub.get(split_path)
122
+ found = urls.get(split_path)
75
123
  if last_path and callable(found):
76
124
  found_path += f'{split_path}/'
77
125
  return found, found_path
@@ -80,13 +128,13 @@ def find_endpoint(path: str) -> tuple[Callable | None, str]:
80
128
  if last_path and callable(endpoint := found.get('')):
81
129
  return endpoint, found_path
82
130
 
83
- sub = found
131
+ urls = found
84
132
  continue
85
133
 
86
134
  # found = None
87
- # sub = {'<id>': <function return_list at 0x7f0757baff60>} (Example)
135
+ # urls = {'<id>': <function return_list at 0x7f0757baff60>} (Example)
88
136
  _continue = False
89
- for key, value in sub.items():
137
+ for key, value in urls.items():
90
138
  if key.startswith('<'):
91
139
  if last_path:
92
140
  if callable(value):
@@ -95,7 +143,7 @@ def find_endpoint(path: str) -> tuple[Callable | None, str]:
95
143
  else:
96
144
  return None, ''
97
145
 
98
- sub = value
146
+ urls = value
99
147
  found_path += f'{key}/'
100
148
  _continue = True
101
149
  break
@@ -106,34 +154,11 @@ def find_endpoint(path: str) -> tuple[Callable | None, str]:
106
154
  return None, ''
107
155
 
108
156
 
109
- def is_recursive_merge(a, b):
110
- both_mapping = isinstance(a, Mapping) and isinstance(b, Mapping)
111
- both_counter = isinstance(a, Counter) and isinstance(b, Counter)
112
- return both_mapping and not both_counter
113
-
114
-
115
- def deepmerge(dst, src):
116
- for key in src:
117
- if key in dst:
118
- if is_recursive_merge(dst[key], src[key]):
119
- deepmerge(dst[key], src[key])
120
- else:
121
- dst[key] = deepcopy(src[key])
122
- else:
123
- dst[key] = deepcopy(src[key])
124
- return dst
125
-
126
-
127
- def merge(destination: MutableMapping, *sources) -> MutableMapping:
128
- """Credit to Travis Clarke --> https://github.com/clarketm/mergedeep"""
129
- return reduce(partial(deepmerge), sources, destination)
130
-
131
-
132
- def finalize_urls(urls: dict) -> dict:
133
- urls_list = list()
134
- for url, endpoint in urls.items():
135
- path = dict()
136
- for single_path in url.split('/')[:-1][::-1]:
137
- path = {single_path: path or endpoint}
138
- urls_list.append(path)
139
- return merge(*urls_list) if urls_list else {}
157
+ def collect_path_variables(request_path: str, found_path: str) -> dict:
158
+ found_path = found_path.removesuffix('/').removeprefix('/')
159
+ request_path = request_path.removesuffix('/').removeprefix('/')
160
+ path_variables = dict()
161
+ for f_path, r_path in zip(found_path.split('/'), request_path.split('/')):
162
+ if f_path.startswith('<'):
163
+ path_variables[f_path[1:-1]] = r_path
164
+ return path_variables
panther/throttling.py CHANGED
@@ -5,7 +5,7 @@ from datetime import timedelta
5
5
  throttling_storage = defaultdict(int)
6
6
 
7
7
 
8
- @dataclass(repr=False, eq=False, match_args=False)
8
+ @dataclass(repr=False, eq=False)
9
9
  class Throttling:
10
10
  rate: int
11
11
  duration: timedelta
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: panther
3
- Version: 1.7.9
3
+ Version: 1.7.10
4
4
  Summary: Fast & Friendly, Web Framework For Building Async APIs
5
5
  Home-page: https://github.com/alirn76/panther
6
6
  Author: Ali RajabNezhad
@@ -8,8 +8,10 @@ Author-email: alirn76@yahoo.com
8
8
  License: MIT
9
9
  Classifier: Operating System :: OS Independent
10
10
  Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3.10
11
12
  Classifier: Programming Language :: Python :: 3.11
12
- Requires-Python: >=3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Requires-Python: >=3.10
13
15
  Description-Content-Type: text/markdown
14
16
  License-File: LICENSE
15
17
  Requires-Dist: bpython (~=0.24)
@@ -30,7 +32,7 @@ Requires-Dist: pymongo (>=4.3.3) ; extra == 'full'
30
32
  <b>Is A Fast & Friendly Web Framework For Building Async APIs With Python 3.11+</b>
31
33
 
32
34
  <p align="center">
33
- <img src="https://github.com/AliRn76/panther/raw/master/docs/docs/images/logo.png" alt="logo" style="width: 200px">
35
+ <img src="https://github.com/AliRn76/panther/raw/master/docs/docs/images/logo-vertical.png" alt="logo" style="width: 450px">
34
36
  </p>
35
37
 
36
38
  >_Full Documentation_ -> [https://pantherpy.github.io](https://pantherpy.github.io)
@@ -39,7 +41,7 @@ Requires-Dist: pymongo (>=4.3.3) ; extra == 'full'
39
41
 
40
42
  ---
41
43
 
42
- ### Why Use Panther ?
44
+ ### Why Use Panther?
43
45
  - Document-oriented Databases ODM ([PantherDB](https://pypi.org/project/pantherdb/), MongoDB)
44
46
  - Visual API Monitoring (In Terminal)
45
47
  - Caching for APIs (In Memory, In Redis)
@@ -1,18 +1,18 @@
1
- panther/__init__.py,sha256=rExEq97I8DmS4Y6UpJ4rVGaYnMuwsXU4SY3hHtchn_A,89
2
- panther/_utils.py,sha256=5UP6Jg-1ORebonKsrwc_gdq09Rhr1j_TBNdbY0G3F08,3651
1
+ panther/__init__.py,sha256=-BZZnfRTWSJLTPMoDH-suqgrCoDYKr1zm0cPs7h6Ev0,90
2
+ panther/_utils.py,sha256=dwspj3BWiiPINIZYvoz586F1e_w2nR9_hCVUXj7MeOY,3227
3
3
  panther/app.py,sha256=GdoWsPBXhQrcdXAaCAyLXYeb7XvX9rQ8-Dgrxy70VnM,6269
4
- panther/authentications.py,sha256=FoBLFh4gf1sM85ZNIoxGaBMvXWAUS7OOYvMJQ94UITM,3098
5
- panther/caching.py,sha256=ja7r2iNJxP6-XUuKBKAcnQQ2s72XYRPpAnbV-YMobxE,2382
6
- panther/configs.py,sha256=Cuhw36owSgbFsCqVTLLx5JKIlTQ1JomDrXR-ojpyOZc,1142
4
+ panther/authentications.py,sha256=3-2niOpcfl6lABld5oep0jnjTieMM9Wc9BWdacDsOys,3551
5
+ panther/caching.py,sha256=r1S-yh3nQrCNcsRwrEE21CA8V5Jsen1Bk_l7KmQKuKQ,2414
6
+ panther/configs.py,sha256=M-ToW-DR3rrV8DclAXKRJvlZS2ZMdONWco1o_2eGnC4,1195
7
7
  panther/exceptions.py,sha256=hXwV0NQSNZZxMtbzDHKIC2363AWMSyFPIHb-DqU2SLw,1150
8
- panther/logger.py,sha256=qnwTt6VoMKHthVvAi8wDb1q86XfhI68E3LqlgpJEZjc,2534
9
- panther/main.py,sha256=vwOhjDwAQTinF-aZLunyL50Y_Qh79eD9PiOzCKjVuQ4,10075
10
- panther/permissions.py,sha256=iKKUNbYH6GKCgS0Au1ildsDsW_gGOs_XlaYiZBtaR-s,383
11
- panther/request.py,sha256=BjpsE_u9uMjG1F1FFHa32LhdhxN7Jlv2rqJg3CiCgh0,4382
8
+ panther/logger.py,sha256=_Cy_NIMu28pszk_SdHrxZlAANqglqBknyy9ZenGIuG4,2615
9
+ panther/main.py,sha256=pTKxyx8ye328E79o9OxB16xj4a9D_QuwCvZYAJ6n-Uk,10442
10
+ panther/permissions.py,sha256=2Pqbrm7Hm2Bu59i0rwsbotjV5w8ZJzeLD-QMWvKOLWg,357
11
+ panther/request.py,sha256=oOnK9Ct3senLr6HmJNSBTmQHQcErqT_P0Dwacl2NE1o,4382
12
12
  panther/response.py,sha256=DUUXqrMnStHMqPcsC9HsqOqWPAF70eLaslZ5lQZNnB4,1593
13
- panther/routings.py,sha256=uCGvbOgbf89siNhLulvqKNZdfmKmdWgSdeW39fVhJHI,4451
13
+ panther/routings.py,sha256=UZKFG6eQZBqgsLhyDkVH-9PvIz8fUqbD-Ye_eeJmOsc,5192
14
14
  panther/status.py,sha256=5mruGJV23VlZo8f6OHLzBLkRRd_dTxKg-4XdYTma7fg,2674
15
- panther/throttling.py,sha256=Xq--i5ttURIzDIUdljF9-N5O_Fc_uhnLUyIyPv4wQaE,249
15
+ panther/throttling.py,sha256=mVa_mGv6w_Ad7LLtV4eG5QpDwwNsk4QjFFi0mIHQBnE,231
16
16
  panther/utils.py,sha256=Zm_aApBZ0Vp_LOCktA10cjGPVIU25gc3V4ss320c9Uw,947
17
17
  panther/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
18
  panther/cli/create_command.py,sha256=F472b-4WH5vC7ZkvZ1qp3MHPr_j5BMM-Zdy2pAllFLI,2910
@@ -23,12 +23,12 @@ panther/cli/template.py,sha256=3InfYeGdjBvCaq5ZlFQNINaw8d8-nDb796hZBgYqv7E,3081
23
23
  panther/cli/utils.py,sha256=rpjSK0T0-0m_UxAnYGPl0-zJ0fVlxmIFRYqhLAddBcY,8539
24
24
  panther/db/__init__.py,sha256=0mo9HwD_JAjJ-kRudbZqWNgzSxJ2t0ewh7Pa-83FhPY,50
25
25
  panther/db/connection.py,sha256=jQkY-JJu4LMHUau8-G6AQzlODwzESkghcLALe6wsR4g,2207
26
- panther/db/models.py,sha256=cv2wTQ32vkgYamf4kSE5dRRGdh7INiHIKzuNqq7Wg3c,999
26
+ panther/db/models.py,sha256=NHvAJO6a2LOJjri_ed4QySrXUZ2UjAM8iQihrXJymO0,954
27
27
  panther/db/utils.py,sha256=Axf7XvkHCr-Ky7c9CJPpQbqgf9kWgW_gVdTqbQlZc94,1289
28
28
  panther/db/queries/__init__.py,sha256=BMffHS9RbHE-AUAeT9C5uY3L-hpDh0WGRduDUQ9Kpuc,41
29
- panther/db/queries/mongodb_queries.py,sha256=uXd81Q2ukJskFDYlNfOiuCCkHuH7URyfZMkD6MUiVkk,3205
30
- panther/db/queries/pantherdb_queries.py,sha256=Ee3Kb5WIGQ-ZDeAYFHYx_Y34f-I0RgLT0XYFEof5FLM,2505
31
- panther/db/queries/queries.py,sha256=s-JpO2M9MnfC5gPttJn2fTj6ru6e0-M5WWpsj1k9yTI,5063
29
+ panther/db/queries/mongodb_queries.py,sha256=84GU96rxWNvNg-Ks9Xw-K3Af9hy3HlUUtpNV2iXxmiQ,3291
30
+ panther/db/queries/pantherdb_queries.py,sha256=QqB5VY2o0X1bxUFoA69M-3RkulKKCpSKmsjJ8OVoF9g,2757
31
+ panther/db/queries/queries.py,sha256=Qq1c3IsAZFlGdFTlMKwFM0upNHcHBzM91uBhguqkod0,5589
32
32
  panther/middlewares/__init__.py,sha256=7RtHuS-MfybnJc6pcBSGhi9teXNhDsnJ3n7h_cXSkJk,66
33
33
  panther/middlewares/base.py,sha256=Php29ckITeGZm6GfauFG3i61bcsb4qoU8RpPLTqsfls,240
34
34
  panther/middlewares/db.py,sha256=C_PevTIaMykJl0NaaMYEfwE_oLdSLfKW2HR9UoPN1dU,508
@@ -37,9 +37,9 @@ panther/middlewares/redis.py,sha256=m_a0QPqUP_OJyuDJtRxCQpn5m_DLdrDIVHHFcytQmAU,
37
37
  panther/panel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
38
  panther/panel/apis.py,sha256=7_OfWdyDARWFr1-ntj0cGF6ZR_25-AAxPSJsa_LPbg0,513
39
39
  panther/panel/urls.py,sha256=uKQGoOlP5lflkcFdAj4oEIzpFVEAR103hAr91PzYelA,169
40
- panther-1.7.9.dist-info/LICENSE,sha256=2aF1hL2aC0zRPjzUkSxJUzZbn2_uLoOkn7DHjzZni-I,1524
41
- panther-1.7.9.dist-info/METADATA,sha256=RzPVCebqJKiHSSzyO4rvm5rCJO_nik2Ohb1k4w7ms00,5490
42
- panther-1.7.9.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
43
- panther-1.7.9.dist-info/entry_points.txt,sha256=6GPxYFGuzVfNB4YpHFJvYex6iWah5_tLnirAHwj2Qsg,51
44
- panther-1.7.9.dist-info/top_level.txt,sha256=VbBs02JGXTIoHMzsX-eLOk2MCbBZzQbLhWiYpI7xI2g,8
45
- panther-1.7.9.dist-info/RECORD,,
40
+ panther-1.7.10.dist-info/LICENSE,sha256=2aF1hL2aC0zRPjzUkSxJUzZbn2_uLoOkn7DHjzZni-I,1524
41
+ panther-1.7.10.dist-info/METADATA,sha256=2kKhFEOp21LyZqICx-j68BCTgdPGOjDUmq42s0QdXyk,5601
42
+ panther-1.7.10.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
43
+ panther-1.7.10.dist-info/entry_points.txt,sha256=6GPxYFGuzVfNB4YpHFJvYex6iWah5_tLnirAHwj2Qsg,51
44
+ panther-1.7.10.dist-info/top_level.txt,sha256=VbBs02JGXTIoHMzsX-eLOk2MCbBZzQbLhWiYpI7xI2g,8
45
+ panther-1.7.10.dist-info/RECORD,,