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 +1 -1
- panther/_utils.py +0 -10
- panther/authentications.py +17 -1
- panther/caching.py +4 -4
- panther/configs.py +12 -12
- panther/db/models.py +14 -20
- panther/db/queries/mongodb_queries.py +12 -5
- panther/db/queries/pantherdb_queries.py +11 -2
- panther/db/queries/queries.py +25 -6
- panther/logger.py +8 -3
- panther/main.py +25 -15
- panther/permissions.py +2 -4
- panther/request.py +7 -7
- panther/routings.py +83 -58
- panther/throttling.py +1 -1
- {panther-1.7.9.dist-info → panther-1.7.10.dist-info}/METADATA +6 -4
- {panther-1.7.9.dist-info → panther-1.7.10.dist-info}/RECORD +21 -21
- {panther-1.7.9.dist-info → panther-1.7.10.dist-info}/LICENSE +0 -0
- {panther-1.7.9.dist-info → panther-1.7.10.dist-info}/WHEEL +0 -0
- {panther-1.7.9.dist-info → panther-1.7.10.dist-info}/entry_points.txt +0 -0
- {panther-1.7.9.dist-info → panther-1.7.10.dist-info}/top_level.txt +0 -0
panther/__init__.py
CHANGED
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
|
panther/authentications.py
CHANGED
@@ -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
|
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
|
-
|
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) ->
|
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
|
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
|
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
|
-
'
|
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 =
|
11
|
+
IDType = str
|
29
12
|
|
30
13
|
|
31
14
|
class Model(PydanticBaseModel, Query):
|
32
|
-
id: IDType | None = Field(
|
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
|
-
|
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,
|
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
|
-
|
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 # # # # #
|
panther/db/queries/queries.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
|
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
|
-
|
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,
|
16
|
-
from panther._utils import http_response, import_class, read_body
|
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.
|
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
|
-
|
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
|
-
|
165
|
-
|
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
|
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
|
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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
94
|
+
dst[key] = deepcopy(src[key])
|
95
|
+
else:
|
96
|
+
dst[key] = deepcopy(src[key])
|
97
|
+
return dst
|
54
98
|
|
55
|
-
|
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
|
-
|
65
|
-
#
|
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 =
|
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
|
-
|
131
|
+
urls = found
|
84
132
|
continue
|
85
133
|
|
86
134
|
# found = None
|
87
|
-
#
|
135
|
+
# urls = {'<id>': <function return_list at 0x7f0757baff60>} (Example)
|
88
136
|
_continue = False
|
89
|
-
for key, value in
|
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
|
-
|
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
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
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
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: panther
|
3
|
-
Version: 1.7.
|
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
|
-
|
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:
|
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
|
2
|
-
panther/_utils.py,sha256=
|
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=
|
5
|
-
panther/caching.py,sha256=
|
6
|
-
panther/configs.py,sha256=
|
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=
|
9
|
-
panther/main.py,sha256=
|
10
|
-
panther/permissions.py,sha256=
|
11
|
-
panther/request.py,sha256=
|
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=
|
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=
|
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=
|
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=
|
30
|
-
panther/db/queries/pantherdb_queries.py,sha256=
|
31
|
-
panther/db/queries/queries.py,sha256=
|
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.
|
41
|
-
panther-1.7.
|
42
|
-
panther-1.7.
|
43
|
-
panther-1.7.
|
44
|
-
panther-1.7.
|
45
|
-
panther-1.7.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|