panther 1.7.8__tar.gz → 1.7.10__tar.gz
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-1.7.8 → panther-1.7.10}/PKG-INFO +6 -4
- {panther-1.7.8 → panther-1.7.10}/README.md +2 -2
- {panther-1.7.8 → panther-1.7.10}/panther/__init__.py +1 -1
- {panther-1.7.8 → panther-1.7.10}/panther/_utils.py +0 -10
- {panther-1.7.8 → panther-1.7.10}/panther/app.py +1 -1
- {panther-1.7.8 → panther-1.7.10}/panther/authentications.py +17 -1
- {panther-1.7.8 → panther-1.7.10}/panther/caching.py +4 -4
- {panther-1.7.8 → panther-1.7.10}/panther/configs.py +13 -13
- panther-1.7.10/panther/db/models.py +38 -0
- {panther-1.7.8 → panther-1.7.10}/panther/db/queries/mongodb_queries.py +12 -5
- {panther-1.7.8 → panther-1.7.10}/panther/db/queries/pantherdb_queries.py +11 -2
- {panther-1.7.8 → panther-1.7.10}/panther/db/queries/queries.py +26 -7
- {panther-1.7.8 → panther-1.7.10}/panther/logger.py +13 -8
- {panther-1.7.8 → panther-1.7.10}/panther/main.py +26 -16
- {panther-1.7.8 → panther-1.7.10}/panther/permissions.py +2 -4
- {panther-1.7.8 → panther-1.7.10}/panther/response.py +2 -2
- {panther-1.7.8 → panther-1.7.10}/panther/routings.py +83 -58
- {panther-1.7.8 → panther-1.7.10}/panther/throttling.py +1 -1
- {panther-1.7.8 → panther-1.7.10}/panther.egg-info/PKG-INFO +6 -4
- {panther-1.7.8 → panther-1.7.10}/panther.egg-info/SOURCES.txt +2 -1
- panther-1.7.10/panther.egg-info/requires.txt +14 -0
- {panther-1.7.8 → panther-1.7.10}/setup.py +14 -12
- panther-1.7.10/tests/test_routing.py +520 -0
- panther-1.7.8/panther/db/models.py +0 -44
- panther-1.7.8/panther.egg-info/requires.txt +0 -14
- {panther-1.7.8 → panther-1.7.10}/LICENSE +0 -0
- {panther-1.7.8 → panther-1.7.10}/panther/cli/__init__.py +0 -0
- {panther-1.7.8 → panther-1.7.10}/panther/cli/create_command.py +0 -0
- {panther-1.7.8 → panther-1.7.10}/panther/cli/main.py +0 -0
- {panther-1.7.8 → panther-1.7.10}/panther/cli/monitor_command.py +0 -0
- {panther-1.7.8 → panther-1.7.10}/panther/cli/run_command.py +0 -0
- {panther-1.7.8 → panther-1.7.10}/panther/cli/template.py +0 -0
- {panther-1.7.8 → panther-1.7.10}/panther/cli/utils.py +0 -0
- {panther-1.7.8 → panther-1.7.10}/panther/db/__init__.py +0 -0
- {panther-1.7.8 → panther-1.7.10}/panther/db/connection.py +0 -0
- {panther-1.7.8 → panther-1.7.10}/panther/db/queries/__init__.py +0 -0
- {panther-1.7.8 → panther-1.7.10}/panther/db/utils.py +0 -0
- {panther-1.7.8 → panther-1.7.10}/panther/exceptions.py +0 -0
- {panther-1.7.8 → panther-1.7.10}/panther/middlewares/__init__.py +0 -0
- {panther-1.7.8 → panther-1.7.10}/panther/middlewares/base.py +0 -0
- {panther-1.7.8 → panther-1.7.10}/panther/middlewares/db.py +0 -0
- {panther-1.7.8 → panther-1.7.10}/panther/middlewares/monitoring.py +0 -0
- {panther-1.7.8 → panther-1.7.10}/panther/middlewares/redis.py +0 -0
- {panther-1.7.8 → panther-1.7.10}/panther/panel/__init__.py +0 -0
- {panther-1.7.8 → panther-1.7.10}/panther/panel/apis.py +0 -0
- {panther-1.7.8 → panther-1.7.10}/panther/panel/urls.py +0 -0
- {panther-1.7.8 → panther-1.7.10}/panther/request.py +7 -7
- {panther-1.7.8 → panther-1.7.10}/panther/status.py +0 -0
- {panther-1.7.8 → panther-1.7.10}/panther/utils.py +0 -0
- {panther-1.7.8 → panther-1.7.10}/panther.egg-info/dependency_links.txt +0 -0
- {panther-1.7.8 → panther-1.7.10}/panther.egg-info/entry_points.txt +0 -0
- {panther-1.7.8 → panther-1.7.10}/panther.egg-info/top_level.txt +0 -0
- {panther-1.7.8 → panther-1.7.10}/pyproject.toml +0 -0
- {panther-1.7.8 → panther-1.7.10}/setup.cfg +0 -0
@@ -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
|
Provides-Extra: full
|
15
17
|
License-File: LICENSE
|
@@ -18,7 +20,7 @@ License-File: LICENSE
|
|
18
20
|
<b>Is A Fast & Friendly Web Framework For Building Async APIs With Python 3.11+</b>
|
19
21
|
|
20
22
|
<p align="center">
|
21
|
-
<img src="https://github.com/AliRn76/panther/raw/master/docs/docs/images/logo.png" alt="logo" style="width:
|
23
|
+
<img src="https://github.com/AliRn76/panther/raw/master/docs/docs/images/logo-vertical.png" alt="logo" style="width: 450px">
|
22
24
|
</p>
|
23
25
|
|
24
26
|
>_Full Documentation_ -> [https://pantherpy.github.io](https://pantherpy.github.io)
|
@@ -27,7 +29,7 @@ License-File: LICENSE
|
|
27
29
|
|
28
30
|
---
|
29
31
|
|
30
|
-
### Why Use Panther
|
32
|
+
### Why Use Panther?
|
31
33
|
- Document-oriented Databases ODM ([PantherDB](https://pypi.org/project/pantherdb/), MongoDB)
|
32
34
|
- Visual API Monitoring (In Terminal)
|
33
35
|
- Caching for APIs (In Memory, In Redis)
|
@@ -2,7 +2,7 @@
|
|
2
2
|
<b>Is A Fast & Friendly Web Framework For Building Async APIs With Python 3.11+</b>
|
3
3
|
|
4
4
|
<p align="center">
|
5
|
-
<img src="https://github.com/AliRn76/panther/raw/master/docs/docs/images/logo.png" alt="logo" style="width:
|
5
|
+
<img src="https://github.com/AliRn76/panther/raw/master/docs/docs/images/logo-vertical.png" alt="logo" style="width: 450px">
|
6
6
|
</p>
|
7
7
|
|
8
8
|
>_Full Documentation_ -> [https://pantherpy.github.io](https://pantherpy.github.io)
|
@@ -11,7 +11,7 @@
|
|
11
11
|
|
12
12
|
---
|
13
13
|
|
14
|
-
### Why Use Panther
|
14
|
+
### Why Use Panther?
|
15
15
|
- Document-oriented Databases ODM ([PantherDB](https://pypi.org/project/pantherdb/), MongoDB)
|
16
16
|
- Visual API Monitoring (In Terminal)
|
17
17
|
- Caching for APIs (In Memory, In Redis)
|
@@ -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
|
@@ -137,7 +137,7 @@ class API:
|
|
137
137
|
def serialize_with_output_model(self, data: any):
|
138
138
|
# Dict
|
139
139
|
if isinstance(data, dict):
|
140
|
-
return self.output_model(**data).
|
140
|
+
return self.output_model(**data).model_dump()
|
141
141
|
|
142
142
|
# Iterable
|
143
143
|
elif isinstance(data, IterableDataTypes):
|
@@ -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'
|
@@ -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
|
|
@@ -2,7 +2,7 @@ from pathlib import Path
|
|
2
2
|
from typing import TypedDict
|
3
3
|
from datetime import timedelta
|
4
4
|
from dataclasses import dataclass
|
5
|
-
from pydantic.
|
5
|
+
from pydantic._internal._model_construction import ModelMetaclass
|
6
6
|
|
7
7
|
from panther.throttling import Throttling
|
8
8
|
|
@@ -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
|
}
|
@@ -0,0 +1,38 @@
|
|
1
|
+
import bson
|
2
|
+
from pydantic import field_validator, Field, BaseModel as PydanticBaseModel
|
3
|
+
|
4
|
+
from panther.configs import config
|
5
|
+
from panther.db.queries import Query
|
6
|
+
|
7
|
+
|
8
|
+
if config['db_engine'] == 'pantherdb':
|
9
|
+
IDType = int
|
10
|
+
else:
|
11
|
+
IDType = str
|
12
|
+
|
13
|
+
|
14
|
+
class Model(PydanticBaseModel, Query):
|
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)
|
27
|
+
|
28
|
+
@property
|
29
|
+
def _id(self):
|
30
|
+
if IDType is int:
|
31
|
+
return self.id
|
32
|
+
else:
|
33
|
+
return bson.ObjectId(self.id) if self.id else None
|
34
|
+
|
35
|
+
|
36
|
+
class BaseUser(Model):
|
37
|
+
first_name: str | None
|
38
|
+
last_name: str | None
|
@@ -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 # # # # #
|
@@ -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
|
@@ -43,7 +51,7 @@ class Query(BaseQuery):
|
|
43
51
|
"""
|
44
52
|
example:
|
45
53
|
>>> from example.app.models import User
|
46
|
-
>>> User.
|
54
|
+
>>> User.find_one(id=1)
|
47
55
|
"""
|
48
56
|
return super().find_one(_data, **kwargs)
|
49
57
|
|
@@ -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')
|
@@ -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):
|
@@ -20,10 +18,10 @@ class LogConfig(BaseModel):
|
|
20
18
|
LOG_LEVEL: str = 'DEBUG'
|
21
19
|
MAX_FILE_SIZE: int = 1024 * 1024 * 100 # 100 MB
|
22
20
|
|
23
|
-
version = 1
|
24
|
-
disable_existing_loggers = False
|
21
|
+
version: int = 1
|
22
|
+
disable_existing_loggers: bool = False
|
25
23
|
|
26
|
-
formatters = {
|
24
|
+
formatters: dict = {
|
27
25
|
'default': {
|
28
26
|
'()': 'uvicorn.logging.DefaultFormatter',
|
29
27
|
'fmt': DEFAULT_LOG_FORMAT,
|
@@ -35,7 +33,7 @@ class LogConfig(BaseModel):
|
|
35
33
|
'datefmt': '%Y-%m-%d %H:%M:%S',
|
36
34
|
},
|
37
35
|
}
|
38
|
-
handlers = {
|
36
|
+
handlers: dict = {
|
39
37
|
'monitoring_file': {
|
40
38
|
'formatter': 'file_formatter',
|
41
39
|
'filename': LOGS_DIR / 'monitoring.log',
|
@@ -63,7 +61,7 @@ class LogConfig(BaseModel):
|
|
63
61
|
'stream': 'ext://sys.stderr',
|
64
62
|
},
|
65
63
|
}
|
66
|
-
loggers = {
|
64
|
+
loggers: dict = {
|
67
65
|
'panther': {
|
68
66
|
'handlers': ['default', 'file'],
|
69
67
|
'level': LOG_LEVEL,
|
@@ -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')
|
@@ -1,9 +1,10 @@
|
|
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
|
6
|
-
from pydantic.
|
7
|
+
from pydantic._internal._model_construction import ModelMetaclass
|
7
8
|
|
8
9
|
from panther import status
|
9
10
|
from panther.request import Request
|
@@ -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
|
@@ -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:
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import orjson as json
|
2
2
|
from types import NoneType
|
3
|
-
from pydantic
|
3
|
+
from pydantic import BaseModel as PydanticBaseModel
|
4
4
|
|
5
5
|
ResponseDataTypes = int | dict | list | tuple | set | str | bool | NoneType
|
6
6
|
IterableDataTypes = list | tuple | set
|
@@ -43,7 +43,7 @@ class Response:
|
|
43
43
|
Make sure the response data is only ResponseDataTypes or Iterable of ResponseDataTypes
|
44
44
|
"""
|
45
45
|
if issubclass(type(data), PydanticBaseModel):
|
46
|
-
return data.
|
46
|
+
return data.model_dump()
|
47
47
|
|
48
48
|
elif isinstance(data, IterableDataTypes):
|
49
49
|
return [cls.clean_data_type(d) for d in data]
|