panther 3.1.3__py3-none-any.whl → 3.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- panther/__init__.py +1 -1
- panther/_load_configs.py +68 -47
- panther/_utils.py +5 -3
- panther/app.py +4 -1
- panther/authentications.py +4 -1
- panther/background_tasks.py +4 -1
- panther/base_request.py +2 -4
- panther/base_websocket.py +4 -1
- panther/caching.py +4 -1
- panther/cli/monitor_command.py +8 -2
- panther/cli/utils.py +13 -5
- panther/configs.py +4 -0
- panther/db/queries/queries.py +15 -3
- panther/db/utils.py +19 -3
- panther/logging.py +68 -0
- panther/main.py +64 -48
- panther/middlewares/redis.py +7 -2
- panther/monitoring.py +41 -0
- panther/panel/apis.py +5 -10
- panther/request.py +4 -1
- panther/routings.py +4 -2
- panther/utils.py +7 -1
- {panther-3.1.3.dist-info → panther-3.1.5.dist-info}/METADATA +2 -2
- {panther-3.1.3.dist-info → panther-3.1.5.dist-info}/RECORD +28 -28
- panther/logger.py +0 -89
- panther/middlewares/monitoring.py +0 -26
- {panther-3.1.3.dist-info → panther-3.1.5.dist-info}/LICENSE +0 -0
- {panther-3.1.3.dist-info → panther-3.1.5.dist-info}/WHEEL +0 -0
- {panther-3.1.3.dist-info → panther-3.1.5.dist-info}/entry_points.txt +0 -0
- {panther-3.1.3.dist-info → panther-3.1.5.dist-info}/top_level.txt +0 -0
panther/__init__.py
CHANGED
panther/_load_configs.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
import ast
|
2
|
-
import os
|
3
2
|
import platform
|
4
3
|
import sys
|
5
4
|
from datetime import timedelta
|
@@ -11,7 +10,6 @@ from pydantic._internal._model_construction import ModelMetaclass
|
|
11
10
|
from panther._utils import import_class
|
12
11
|
from panther.configs import JWTConfig, config
|
13
12
|
from panther.exceptions import PantherException
|
14
|
-
from panther.middlewares import BaseMiddleware
|
15
13
|
from panther.routings import finalize_urls, flatten_urls
|
16
14
|
from panther.throttling import Throttling
|
17
15
|
|
@@ -73,6 +71,8 @@ def load_default_cache_exp(configs: dict, /) -> timedelta | None:
|
|
73
71
|
|
74
72
|
def load_middlewares(configs: dict, /) -> list:
|
75
73
|
"""Collect The Middlewares & Set db_engine If One Of Middlewares Was For DB"""
|
74
|
+
from panther.middlewares import BaseMiddleware
|
75
|
+
|
76
76
|
middlewares = []
|
77
77
|
|
78
78
|
for path, data in configs.get('MIDDLEWARES', []):
|
@@ -113,52 +113,73 @@ def collect_all_models() -> list[dict]:
|
|
113
113
|
"""Collecting all models for panel APIs"""
|
114
114
|
from panther.db.models import Model
|
115
115
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
for f in
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
116
|
+
# Just load all the python files from 'base_dir',
|
117
|
+
# so Model.__subclasses__ can find all the subclasses
|
118
|
+
slash = '\\' if platform.system() == 'Windows' else '/'
|
119
|
+
python_files = [
|
120
|
+
f for f in config['base_dir'].rglob('*.py')
|
121
|
+
if not f.name.startswith('_') and 'site-packages' not in f.parents._parts
|
122
|
+
]
|
123
|
+
for file in python_files:
|
124
|
+
# Analyse the file
|
125
|
+
with Path(file).open() as f:
|
126
|
+
node = ast.parse(f.read())
|
127
|
+
|
128
|
+
model_imported = False
|
129
|
+
panther_imported = False
|
130
|
+
panther_called = False
|
131
|
+
for n in node.body:
|
132
|
+
match n:
|
133
|
+
|
134
|
+
# from panther.db import Model
|
135
|
+
case ast.ImportFrom(module='panther.db', names=[ast.alias(name='Model')]):
|
136
|
+
model_imported = True
|
137
|
+
|
138
|
+
# from panther.db.models import ..., Model, ...
|
139
|
+
case ast.ImportFrom(module='panther.db.models', names=[*names]):
|
140
|
+
try:
|
141
|
+
next(v for v in names if v.name == 'Model')
|
142
|
+
model_imported = True
|
143
|
+
except StopIteration:
|
144
|
+
pass
|
145
|
+
|
146
|
+
# from panther import Panther, ...
|
147
|
+
case ast.ImportFrom(module='panther', names=[ast.alias(name='Panther'), *_]):
|
148
|
+
panther_imported = True
|
149
|
+
|
150
|
+
# from panther import ..., Panther
|
151
|
+
case ast.ImportFrom(module='panther', names=[*_, ast.alias(name='Panther')]):
|
152
|
+
panther_imported = True
|
153
|
+
|
154
|
+
# ... = Panther(...)
|
155
|
+
case ast.Assign(value=ast.Call(func=ast.Name(id='Panther'))):
|
156
|
+
panther_called = True
|
157
|
+
|
158
|
+
# Panther() should not be called in the file and Model() should be imported,
|
159
|
+
# We check the import of the Panther to make sure he is calling the panther.Panther and not any Panther
|
160
|
+
if panther_imported and panther_called or not model_imported:
|
161
|
+
continue
|
162
|
+
|
163
|
+
# Load the module
|
164
|
+
dotted_f = str(file).removeprefix(f'{config["base_dir"]}{slash}').removesuffix('.py').replace(slash, '.')
|
165
|
+
import_module(dotted_f)
|
166
|
+
|
167
|
+
return [
|
168
|
+
{
|
169
|
+
'name': m.__name__,
|
170
|
+
'module': m.__module__,
|
171
|
+
'class': m
|
172
|
+
} for m in Model.__subclasses__() if m.__module__ != 'panther.db.models'
|
173
|
+
]
|
174
|
+
|
175
|
+
|
176
|
+
def load_urls(configs: dict, /, urls: dict | None) -> tuple[dict, dict]:
|
177
|
+
"""
|
178
|
+
Return tuple of all urls (as a flat dict) and (as a nested dict)
|
179
|
+
"""
|
159
180
|
if isinstance(urls, dict):
|
160
181
|
collected_urls = flatten_urls(urls)
|
161
|
-
return finalize_urls(collected_urls)
|
182
|
+
return collected_urls, finalize_urls(collected_urls)
|
162
183
|
|
163
184
|
if (url_routing := configs.get('URLs')) is None:
|
164
185
|
raise _exception_handler(field='URLs', error='is required.')
|
@@ -182,7 +203,7 @@ def load_urls(configs: dict, /, urls: dict | None) -> dict:
|
|
182
203
|
raise _exception_handler(field='URLs', error='should point to a dict.')
|
183
204
|
|
184
205
|
collected_urls = flatten_urls(imported_urls)
|
185
|
-
return finalize_urls(collected_urls)
|
206
|
+
return collected_urls, finalize_urls(collected_urls)
|
186
207
|
|
187
208
|
|
188
209
|
def load_panel_urls() -> dict:
|
panther/_utils.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
import importlib
|
2
|
+
import logging
|
2
3
|
import re
|
3
4
|
from collections.abc import Callable
|
4
5
|
from traceback import TracebackException
|
@@ -8,7 +9,9 @@ import orjson as json
|
|
8
9
|
|
9
10
|
from panther import status
|
10
11
|
from panther.file_handler import File
|
11
|
-
|
12
|
+
|
13
|
+
|
14
|
+
logger = logging.getLogger('panther')
|
12
15
|
|
13
16
|
|
14
17
|
async def _http_response_start(send: Callable, /, headers: dict, status_code: int) -> None:
|
@@ -42,8 +45,7 @@ async def http_response(
|
|
42
45
|
elif status_code == status.HTTP_204_NO_CONTENT or body == b'null':
|
43
46
|
body = None
|
44
47
|
|
45
|
-
|
46
|
-
await monitoring.after(status_code=status_code)
|
48
|
+
await monitoring.after(status_code)
|
47
49
|
|
48
50
|
await _http_response_start(send, headers=headers, status_code=status_code)
|
49
51
|
await _http_response_body(send, body=body)
|
panther/app.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
import functools
|
2
2
|
from collections.abc import Callable
|
3
3
|
from datetime import datetime, timedelta
|
4
|
+
import logging
|
4
5
|
from typing import Literal
|
5
6
|
|
6
7
|
from orjson import JSONDecodeError
|
@@ -18,7 +19,6 @@ from panther.exceptions import (
|
|
18
19
|
MethodNotAllowed,
|
19
20
|
ThrottlingException,
|
20
21
|
)
|
21
|
-
from panther.logger import logger
|
22
22
|
from panther.request import Request
|
23
23
|
from panther.response import Response
|
24
24
|
from panther.throttling import Throttling, throttling_storage
|
@@ -27,6 +27,9 @@ from panther.utils import round_datetime
|
|
27
27
|
__all__ = ('API', 'GenericAPI')
|
28
28
|
|
29
29
|
|
30
|
+
logger = logging.getLogger('panther')
|
31
|
+
|
32
|
+
|
30
33
|
class API:
|
31
34
|
def __init__(
|
32
35
|
self,
|
panther/authentications.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
import time
|
2
2
|
from abc import abstractmethod
|
3
|
+
import logging
|
3
4
|
from typing import Literal
|
4
5
|
|
5
6
|
from jose import JWTError, jwt
|
@@ -7,10 +8,12 @@ from jose import JWTError, jwt
|
|
7
8
|
from panther.configs import config
|
8
9
|
from panther.db.models import BaseUser, IDType, Model
|
9
10
|
from panther.exceptions import AuthenticationException
|
10
|
-
from panther.logger import logger
|
11
11
|
from panther.request import Request
|
12
12
|
|
13
13
|
|
14
|
+
logger = logging.getLogger('panther')
|
15
|
+
|
16
|
+
|
14
17
|
class BaseAuthentication:
|
15
18
|
@classmethod
|
16
19
|
@abstractmethod
|
panther/background_tasks.py
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
import asyncio
|
2
2
|
import datetime
|
3
|
+
import logging
|
3
4
|
import sys
|
4
5
|
import time
|
5
6
|
from threading import Thread
|
6
7
|
from typing import Callable, Literal
|
7
8
|
|
8
9
|
from panther._utils import is_function_async
|
9
|
-
from panther.logger import logger
|
10
10
|
from panther.utils import Singleton
|
11
11
|
|
12
12
|
|
@@ -16,6 +16,9 @@ __all__ = (
|
|
16
16
|
)
|
17
17
|
|
18
18
|
|
19
|
+
logger = logging.getLogger('panther')
|
20
|
+
|
21
|
+
|
19
22
|
if sys.version_info.minor >= 11:
|
20
23
|
from typing import Self
|
21
24
|
else:
|
panther/base_request.py
CHANGED
@@ -2,8 +2,6 @@ from collections import namedtuple
|
|
2
2
|
from collections.abc import Callable
|
3
3
|
from dataclasses import dataclass
|
4
4
|
|
5
|
-
from panther.db import Model
|
6
|
-
|
7
5
|
|
8
6
|
@dataclass(frozen=True)
|
9
7
|
class Headers:
|
@@ -86,8 +84,8 @@ class BaseRequest:
|
|
86
84
|
return self.scope['scheme']
|
87
85
|
|
88
86
|
@property
|
89
|
-
def user(self)
|
87
|
+
def user(self):
|
90
88
|
return self._user
|
91
89
|
|
92
|
-
def set_user(self, user
|
90
|
+
def set_user(self, user) -> None:
|
93
91
|
self._user = user
|
panther/base_websocket.py
CHANGED
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import asyncio
|
4
4
|
import contextlib
|
5
|
+
import logging
|
5
6
|
from typing import TYPE_CHECKING
|
6
7
|
|
7
8
|
import orjson as json
|
@@ -10,13 +11,15 @@ from panther import status
|
|
10
11
|
from panther._utils import generate_ws_connection_id
|
11
12
|
from panther.base_request import BaseRequest
|
12
13
|
from panther.configs import config
|
13
|
-
from panther.logger import logger
|
14
14
|
from panther.utils import Singleton
|
15
15
|
|
16
16
|
if TYPE_CHECKING:
|
17
17
|
from redis import Redis
|
18
18
|
|
19
19
|
|
20
|
+
logger = logging.getLogger('panther')
|
21
|
+
|
22
|
+
|
20
23
|
class WebsocketConnections(Singleton):
|
21
24
|
def __init__(self):
|
22
25
|
self.connections = {}
|
panther/caching.py
CHANGED
@@ -1,16 +1,19 @@
|
|
1
1
|
from collections import namedtuple
|
2
2
|
from datetime import timedelta
|
3
|
+
import logging
|
3
4
|
from types import NoneType
|
4
5
|
|
5
6
|
import orjson as json
|
6
7
|
|
7
8
|
from panther.configs import config
|
8
9
|
from panther.db.connection import redis
|
9
|
-
from panther.logger import logger
|
10
10
|
from panther.request import Request
|
11
11
|
from panther.response import Response, ResponseDataTypes
|
12
12
|
from panther.utils import generate_hash_value_from_string
|
13
13
|
|
14
|
+
|
15
|
+
logger = logging.getLogger('panther')
|
16
|
+
|
14
17
|
caches = {}
|
15
18
|
CachedResponse = namedtuple('Cached', ['data', 'status_code'])
|
16
19
|
|
panther/cli/monitor_command.py
CHANGED
@@ -12,9 +12,12 @@ from rich.panel import Panel
|
|
12
12
|
from rich.table import Table
|
13
13
|
from watchfiles import watch
|
14
14
|
|
15
|
+
from panther.cli.utils import cli_error
|
16
|
+
from panther.configs import config
|
17
|
+
|
15
18
|
|
16
19
|
def monitor() -> None:
|
17
|
-
monitoring_log_file = 'logs/monitoring.log'
|
20
|
+
monitoring_log_file = Path(config['base_dir'] / 'logs' / 'monitoring.log')
|
18
21
|
|
19
22
|
def _generate_table(rows: deque) -> Panel:
|
20
23
|
layout = Layout()
|
@@ -42,7 +45,10 @@ def monitor() -> None:
|
|
42
45
|
border_style='bright_blue',
|
43
46
|
)
|
44
47
|
|
45
|
-
|
48
|
+
if not monitoring_log_file.exists():
|
49
|
+
return cli_error('Monitoring file not found. (You need at least one monitoring record for this action)')
|
50
|
+
|
51
|
+
with monitoring_log_file.open() as f:
|
46
52
|
f.readlines() # Set cursor at the end of file
|
47
53
|
|
48
54
|
_, init_lines_count = os.get_terminal_size()
|
panther/cli/utils.py
CHANGED
@@ -1,7 +1,10 @@
|
|
1
|
-
|
2
|
-
from panther.logger import logger
|
1
|
+
import logging
|
3
2
|
from rich import print as rprint
|
4
3
|
|
4
|
+
|
5
|
+
logger = logging.getLogger('panther')
|
6
|
+
|
7
|
+
|
5
8
|
logo = r"""│ ____ __ __ │
|
6
9
|
│ /\ _`\ /\ \__/\ \ │
|
7
10
|
│ \ \ \L\ \ __ ___\ \ ,_\ \ \___ __ _ __ │
|
@@ -78,21 +81,26 @@ def print_uvicorn_help_message():
|
|
78
81
|
def print_info(config: dict):
|
79
82
|
mo = config['monitoring']
|
80
83
|
lq = config['log_queries']
|
81
|
-
rc = redis.is_connected
|
82
84
|
bt = config['background_tasks']
|
85
|
+
ws = config['has_ws']
|
83
86
|
bd = '{0:<39}'.format(str(config['base_dir']))
|
84
87
|
if len(bd) > 39:
|
85
88
|
bd = f'{bd[:36]}...'
|
86
89
|
|
90
|
+
if config['monitoring']:
|
91
|
+
monitor = '│ * Run "panther monitor" in another session for Monitoring│'
|
92
|
+
else:
|
93
|
+
monitor = f'│{58 * " "}│'
|
94
|
+
|
87
95
|
info_message = f"""
|
88
96
|
╭{58 * '─'}╮
|
89
97
|
{logo}│{58 * ' '}│
|
90
|
-
│ │
|
91
98
|
│ Monitoring: {mo} \t │
|
92
99
|
│ Log Queries: {lq} \t │
|
93
|
-
│ Redis Is Connected: {rc} \t │
|
94
100
|
│ Background Tasks: {bt} \t │
|
101
|
+
│ Websocket: {ws} \t │
|
95
102
|
│ Base directory: {bd}│
|
103
|
+
{monitor}
|
96
104
|
╰{58 * '─'}╯
|
97
105
|
"""
|
98
106
|
rprint(info_message)
|
panther/configs.py
CHANGED
@@ -41,10 +41,12 @@ class Config(TypedDict):
|
|
41
41
|
authentication: ModelMetaclass | None
|
42
42
|
jwt_config: JWTConfig | None
|
43
43
|
models: list[dict]
|
44
|
+
flat_urls: dict
|
44
45
|
urls: dict
|
45
46
|
db_engine: str
|
46
47
|
websocket_connections: any # type: WebsocketConnections
|
47
48
|
background_tasks: bool
|
49
|
+
has_ws: bool
|
48
50
|
|
49
51
|
|
50
52
|
config: Config = {
|
@@ -60,8 +62,10 @@ config: Config = {
|
|
60
62
|
'authentication': None,
|
61
63
|
'jwt_config': None,
|
62
64
|
'models': [],
|
65
|
+
'flat_urls': {},
|
63
66
|
'urls': {},
|
64
67
|
'db_engine': '',
|
65
68
|
'websocket_connections': None,
|
66
69
|
'background_tasks': False,
|
70
|
+
'has_ws': False,
|
67
71
|
}
|
panther/db/queries/queries.py
CHANGED
@@ -6,7 +6,7 @@ from panther import status
|
|
6
6
|
from panther.configs import config
|
7
7
|
from panther.db.queries.mongodb_queries import BaseMongoDBQuery
|
8
8
|
from panther.db.queries.pantherdb_queries import BasePantherDBQuery
|
9
|
-
from panther.db.utils import log_query
|
9
|
+
from panther.db.utils import log_query, check_connection
|
10
10
|
from panther.exceptions import APIException, DBException
|
11
11
|
|
12
12
|
BaseQuery = BasePantherDBQuery if config['db_engine'] == 'pantherdb' else BaseMongoDBQuery
|
@@ -55,6 +55,7 @@ class Query(BaseQuery):
|
|
55
55
|
|
56
56
|
# # # # # Find # # # # #
|
57
57
|
@classmethod
|
58
|
+
@check_connection
|
58
59
|
@log_query
|
59
60
|
def find_one(cls, _data: dict | None = None, /, **kwargs) -> Self | None:
|
60
61
|
"""
|
@@ -66,6 +67,7 @@ class Query(BaseQuery):
|
|
66
67
|
return super().find_one(_data, **kwargs)
|
67
68
|
|
68
69
|
@classmethod
|
70
|
+
@check_connection
|
69
71
|
@log_query
|
70
72
|
def find(cls, _data: dict | None = None, /, **kwargs) -> list[Self]:
|
71
73
|
"""
|
@@ -77,6 +79,7 @@ class Query(BaseQuery):
|
|
77
79
|
return super().find(_data, **kwargs)
|
78
80
|
|
79
81
|
@classmethod
|
82
|
+
@check_connection
|
80
83
|
@log_query
|
81
84
|
def first(cls, _data: dict | None = None, /, **kwargs) -> Self | None:
|
82
85
|
"""
|
@@ -89,6 +92,7 @@ class Query(BaseQuery):
|
|
89
92
|
return super().first(_data, **kwargs)
|
90
93
|
|
91
94
|
@classmethod
|
95
|
+
@check_connection
|
92
96
|
@log_query
|
93
97
|
def last(cls, _data: dict | None = None, /, **kwargs) -> Self | None:
|
94
98
|
"""
|
@@ -101,6 +105,7 @@ class Query(BaseQuery):
|
|
101
105
|
|
102
106
|
# # # # # Count # # # # #
|
103
107
|
@classmethod
|
108
|
+
@check_connection
|
104
109
|
@log_query
|
105
110
|
def count(cls, _data: dict | None = None, /, **kwargs) -> int:
|
106
111
|
"""
|
@@ -113,6 +118,7 @@ class Query(BaseQuery):
|
|
113
118
|
|
114
119
|
# # # # # Insert # # # # #
|
115
120
|
@classmethod
|
121
|
+
@check_connection
|
116
122
|
@log_query
|
117
123
|
def insert_one(cls, _data: dict | None = None, /, **kwargs) -> Self:
|
118
124
|
"""
|
@@ -125,12 +131,14 @@ class Query(BaseQuery):
|
|
125
131
|
return super().insert_one(_data, **kwargs)
|
126
132
|
|
127
133
|
@classmethod
|
134
|
+
@check_connection
|
128
135
|
@log_query
|
129
136
|
def insert_many(cls, _data: dict | None = None, /, **kwargs):
|
130
137
|
msg = 'insert_many() is not supported yet.'
|
131
138
|
raise DBException(msg)
|
132
139
|
|
133
140
|
# # # # # Delete # # # # #
|
141
|
+
@check_connection
|
134
142
|
@log_query
|
135
143
|
def delete(self) -> None:
|
136
144
|
"""
|
@@ -143,6 +151,7 @@ class Query(BaseQuery):
|
|
143
151
|
return super().delete()
|
144
152
|
|
145
153
|
@classmethod
|
154
|
+
@check_connection
|
146
155
|
@log_query
|
147
156
|
def delete_one(cls, _data: dict | None = None, /, **kwargs) -> bool:
|
148
157
|
"""
|
@@ -154,6 +163,7 @@ class Query(BaseQuery):
|
|
154
163
|
return super().delete_one(_data, **kwargs)
|
155
164
|
|
156
165
|
@classmethod
|
166
|
+
@check_connection
|
157
167
|
@log_query
|
158
168
|
def delete_many(cls, _data: dict | None = None, /, **kwargs) -> int:
|
159
169
|
"""
|
@@ -165,6 +175,7 @@ class Query(BaseQuery):
|
|
165
175
|
return super().delete_many(_data, **kwargs)
|
166
176
|
|
167
177
|
# # # # # Update # # # # #
|
178
|
+
@check_connection
|
168
179
|
@log_query
|
169
180
|
def update(self, **kwargs) -> None:
|
170
181
|
"""
|
@@ -178,6 +189,7 @@ class Query(BaseQuery):
|
|
178
189
|
return super().update(**kwargs)
|
179
190
|
|
180
191
|
@classmethod
|
192
|
+
@check_connection
|
181
193
|
@log_query
|
182
194
|
def update_one(cls, _filter: dict, _data: dict | None = None, /, **kwargs) -> bool:
|
183
195
|
"""
|
@@ -190,6 +202,7 @@ class Query(BaseQuery):
|
|
190
202
|
return super().update_one(_filter, _data, **kwargs)
|
191
203
|
|
192
204
|
@classmethod
|
205
|
+
@check_connection
|
193
206
|
@log_query
|
194
207
|
def update_many(cls, _filter: dict, _data: dict | None = None, /, **kwargs) -> int:
|
195
208
|
"""
|
@@ -203,7 +216,6 @@ class Query(BaseQuery):
|
|
203
216
|
|
204
217
|
# # # # # Other # # # # #
|
205
218
|
@classmethod
|
206
|
-
@log_query
|
207
219
|
def find_or_insert(cls, **kwargs) -> tuple[bool, any]:
|
208
220
|
"""
|
209
221
|
Example:
|
@@ -216,7 +228,6 @@ class Query(BaseQuery):
|
|
216
228
|
return True, cls.insert_one(**kwargs)
|
217
229
|
|
218
230
|
@classmethod
|
219
|
-
@log_query
|
220
231
|
def find_one_or_raise(cls, **kwargs) -> Self:
|
221
232
|
"""
|
222
233
|
Example:
|
@@ -232,6 +243,7 @@ class Query(BaseQuery):
|
|
232
243
|
status_code=status.HTTP_404_NOT_FOUND,
|
233
244
|
)
|
234
245
|
|
246
|
+
@check_connection
|
235
247
|
@log_query
|
236
248
|
def save(self, **kwargs) -> None:
|
237
249
|
"""
|
panther/db/utils.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import logging
|
1
2
|
import operator
|
2
3
|
from functools import reduce
|
3
4
|
from time import perf_counter
|
@@ -5,23 +6,38 @@ from time import perf_counter
|
|
5
6
|
import bson
|
6
7
|
|
7
8
|
from panther.configs import config
|
8
|
-
|
9
|
+
|
10
|
+
|
11
|
+
logger = logging.getLogger('query')
|
9
12
|
|
10
13
|
|
11
14
|
def log_query(func):
|
12
15
|
def log(*args, **kwargs):
|
16
|
+
# Check Database Connection
|
17
|
+
if config['db_engine'] == '':
|
18
|
+
msg = "You don't have active database connection, Check your middlewares"
|
19
|
+
raise NotImplementedError(msg)
|
20
|
+
|
13
21
|
if config['log_queries'] is False:
|
14
22
|
return func(*args, **kwargs)
|
15
23
|
start = perf_counter()
|
16
24
|
response = func(*args, **kwargs)
|
17
25
|
end = perf_counter()
|
18
26
|
class_name = args[0].__name__ if hasattr(args[0], '__name__') else args[0].__class__.__name__
|
19
|
-
|
27
|
+
logger.info(f'\033[1mQuery -->\033[0m {class_name}.{func.__name__}() --> {(end - start) * 1_000:.2} ms')
|
20
28
|
return response
|
21
|
-
|
22
29
|
return log
|
23
30
|
|
24
31
|
|
32
|
+
def check_connection(func):
|
33
|
+
def wrapper(*args, **kwargs):
|
34
|
+
if config['db_engine'] == '':
|
35
|
+
msg = "You don't have active database connection, Check your middlewares"
|
36
|
+
raise NotImplementedError(msg)
|
37
|
+
return func(*args, **kwargs)
|
38
|
+
return wrapper
|
39
|
+
|
40
|
+
|
25
41
|
def prepare_id_for_query(*args, is_mongo: bool = False):
|
26
42
|
for d in args:
|
27
43
|
if d is None:
|
panther/logging.py
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
import logging
|
2
|
+
from pathlib import Path
|
3
|
+
from panther.configs import config
|
4
|
+
|
5
|
+
LOGS_DIR = config['base_dir'] / 'logs'
|
6
|
+
|
7
|
+
|
8
|
+
class FileHandler(logging.FileHandler):
|
9
|
+
def __init__(self, filename, mode='a', encoding=None, delay=False, errors=None):
|
10
|
+
Path(LOGS_DIR).mkdir(exist_ok=True)
|
11
|
+
logging.FileHandler.__init__(self, filename, mode=mode, encoding=encoding, delay=delay, errors=errors)
|
12
|
+
|
13
|
+
|
14
|
+
LOGGING = {
|
15
|
+
'version': 1,
|
16
|
+
'disable_existing_loggers': False,
|
17
|
+
'formatters': {
|
18
|
+
'default': {
|
19
|
+
'()': 'uvicorn.logging.DefaultFormatter',
|
20
|
+
'fmt': '%(levelprefix)s | %(asctime)s | %(message)s',
|
21
|
+
'datefmt': '%Y-%m-%d %H:%M:%S',
|
22
|
+
},
|
23
|
+
'panther_file_formatter': {
|
24
|
+
'()': 'uvicorn.logging.DefaultFormatter',
|
25
|
+
'fmt': '%(asctime)s | %(message)s',
|
26
|
+
'datefmt': '%Y-%m-%d %H:%M:%S',
|
27
|
+
},
|
28
|
+
},
|
29
|
+
'handlers': {
|
30
|
+
'panther_file': {
|
31
|
+
'formatter': 'panther_file_formatter',
|
32
|
+
'filename': LOGS_DIR / 'main.log',
|
33
|
+
'class': 'panther.logging.FileHandler',
|
34
|
+
'delay': True,
|
35
|
+
},
|
36
|
+
'monitoring_file': {
|
37
|
+
'formatter': 'panther_file_formatter',
|
38
|
+
'filename': LOGS_DIR / 'monitoring.log',
|
39
|
+
'class': 'panther.logging.FileHandler',
|
40
|
+
'delay': True,
|
41
|
+
},
|
42
|
+
'query_file': {
|
43
|
+
'formatter': 'panther_file_formatter',
|
44
|
+
'filename': LOGS_DIR / 'query.log',
|
45
|
+
'class': 'panther.logging.FileHandler',
|
46
|
+
'delay': True,
|
47
|
+
},
|
48
|
+
'default': {
|
49
|
+
'formatter': 'default',
|
50
|
+
'class': 'logging.StreamHandler',
|
51
|
+
'stream': 'ext://sys.stderr',
|
52
|
+
},
|
53
|
+
},
|
54
|
+
'loggers': {
|
55
|
+
'panther': {
|
56
|
+
'handlers': ['default', 'panther_file'],
|
57
|
+
'level': 'DEBUG',
|
58
|
+
},
|
59
|
+
'monitoring': {
|
60
|
+
'handlers': ['monitoring_file'],
|
61
|
+
'level': 'DEBUG',
|
62
|
+
},
|
63
|
+
'query': {
|
64
|
+
'handlers': ['default', 'query_file'],
|
65
|
+
'level': 'DEBUG',
|
66
|
+
}
|
67
|
+
}
|
68
|
+
}
|
panther/main.py
CHANGED
@@ -1,10 +1,13 @@
|
|
1
1
|
import contextlib
|
2
|
+
import logging
|
2
3
|
import sys
|
3
4
|
import types
|
4
5
|
from collections.abc import Callable
|
6
|
+
from logging.config import dictConfig
|
5
7
|
from pathlib import Path
|
6
8
|
from threading import Thread
|
7
9
|
|
10
|
+
import panther.logging
|
8
11
|
from panther import status
|
9
12
|
from panther._load_configs import *
|
10
13
|
from panther._utils import clean_traceback_message, http_response
|
@@ -12,18 +15,18 @@ from panther.background_tasks import background_tasks
|
|
12
15
|
from panther.cli.utils import print_info
|
13
16
|
from panther.configs import config
|
14
17
|
from panther.exceptions import APIException, PantherException
|
15
|
-
from panther.
|
18
|
+
from panther.monitoring import Monitoring
|
16
19
|
from panther.request import Request
|
17
20
|
from panther.response import Response
|
18
21
|
from panther.routings import collect_path_variables, find_endpoint
|
19
22
|
|
20
|
-
|
23
|
+
|
24
|
+
dictConfig(panther.logging.LOGGING)
|
25
|
+
logger = logging.getLogger('panther')
|
21
26
|
|
22
27
|
|
23
28
|
class Panther:
|
24
29
|
def __init__(self, name: str, configs=None, urls: dict | None = None):
|
25
|
-
from panther.logger import logger
|
26
|
-
|
27
30
|
self._configs = configs
|
28
31
|
self._urls = urls
|
29
32
|
config['base_dir'] = Path(name).resolve().parent
|
@@ -37,22 +40,21 @@ class Panther:
|
|
37
40
|
logger.error(clean_traceback_message(e))
|
38
41
|
sys.exit()
|
39
42
|
|
40
|
-
#
|
41
|
-
|
42
|
-
target=self.websocket_connections,
|
43
|
-
daemon=True,
|
44
|
-
args=(self.ws_redis_connection,),
|
45
|
-
).start()
|
43
|
+
# Monitoring
|
44
|
+
self.monitoring = Monitoring(is_active=config['monitoring'])
|
46
45
|
|
47
46
|
# Print Info
|
48
47
|
print_info(config)
|
49
|
-
|
50
|
-
|
51
|
-
if
|
52
|
-
|
48
|
+
|
49
|
+
# Start Websocket Listener (Redis Required)
|
50
|
+
if config['has_ws']:
|
51
|
+
Thread(
|
52
|
+
target=self.websocket_connections,
|
53
|
+
daemon=True,
|
54
|
+
args=(self.ws_redis_connection,),
|
55
|
+
).start()
|
53
56
|
|
54
57
|
def load_configs(self) -> None:
|
55
|
-
from panther.logger import logger
|
56
58
|
|
57
59
|
# Check & Read The Configs File
|
58
60
|
self.configs = load_configs_file(self._configs)
|
@@ -71,58 +73,74 @@ class Panther:
|
|
71
73
|
config['jwt_config'] = load_jwt_config(self.configs)
|
72
74
|
config['models'] = collect_all_models()
|
73
75
|
|
74
|
-
# Create websocket connections instance
|
75
|
-
from panther.websocket import WebsocketConnections
|
76
|
-
|
77
|
-
config['websocket_connections'] = self.websocket_connections = WebsocketConnections()
|
78
|
-
# Websocket Redis Connection
|
79
|
-
for middleware in config['middlewares']:
|
80
|
-
if middleware.__class__.__name__ == 'RedisMiddleware':
|
81
|
-
self.ws_redis_connection = middleware.redis_connection_for_ws()
|
82
|
-
break
|
83
|
-
else:
|
84
|
-
self.ws_redis_connection = None
|
85
|
-
|
86
76
|
# Initialize Background Tasks
|
87
77
|
if config['background_tasks']:
|
88
78
|
background_tasks.initialize()
|
89
79
|
|
90
|
-
# Load URLs should be the last
|
80
|
+
# Load URLs should be one of the last calls in load_configs,
|
91
81
|
# because it will read all files and loads them.
|
92
|
-
config['urls'] = load_urls(self.configs, urls=self._urls)
|
82
|
+
config['flat_urls'], config['urls'] = load_urls(self.configs, urls=self._urls)
|
93
83
|
config['urls']['_panel'] = load_panel_urls()
|
94
84
|
|
85
|
+
self._create_ws_connections_instance()
|
86
|
+
|
87
|
+
def _create_ws_connections_instance(self):
|
88
|
+
from panther.base_websocket import Websocket
|
89
|
+
from panther.websocket import WebsocketConnections
|
90
|
+
|
91
|
+
# Check do we have ws endpoint
|
92
|
+
for endpoint in config['flat_urls'].values():
|
93
|
+
if not isinstance(endpoint, types.FunctionType) and issubclass(endpoint, Websocket):
|
94
|
+
config['has_ws'] = True
|
95
|
+
break
|
96
|
+
else:
|
97
|
+
config['has_ws'] = False
|
98
|
+
|
99
|
+
# Create websocket connections instance
|
100
|
+
if config['has_ws']:
|
101
|
+
config['websocket_connections'] = self.websocket_connections = WebsocketConnections()
|
102
|
+
# Websocket Redis Connection
|
103
|
+
for middleware in config['middlewares']:
|
104
|
+
if middleware.__class__.__name__ == 'RedisMiddleware':
|
105
|
+
self.ws_redis_connection = middleware.redis_connection_for_ws()
|
106
|
+
break
|
107
|
+
else:
|
108
|
+
self.ws_redis_connection = None
|
109
|
+
|
95
110
|
async def __call__(self, scope: dict, receive: Callable, send: Callable) -> None:
|
96
111
|
"""
|
97
112
|
1.
|
98
|
-
|
99
|
-
tg.create_task(self.run(scope, receive, send))
|
113
|
+
await func(scope, receive, send)
|
100
114
|
2.
|
101
|
-
|
115
|
+
async with asyncio.TaskGroup() as tg:
|
116
|
+
tg.create_task(func(scope, receive, send))
|
102
117
|
3.
|
103
118
|
async with anyio.create_task_group() as task_group:
|
104
|
-
task_group.start_soon(
|
105
|
-
await anyio.to_thread.run_sync(
|
119
|
+
task_group.start_soon(func, scope, receive, send)
|
120
|
+
await anyio.to_thread.run_sync(func, scope, receive, send)
|
106
121
|
4.
|
107
122
|
with ProcessPoolExecutor() as e:
|
108
|
-
e.submit(
|
123
|
+
e.submit(func, scope, receive, send)
|
109
124
|
"""
|
110
125
|
func = self.handle_http if scope['type'] == 'http' else self.handle_ws
|
111
126
|
await func(scope=scope, receive=receive, send=send)
|
112
127
|
|
113
128
|
async def handle_ws(self, scope: dict, receive: Callable, send: Callable) -> None:
|
114
|
-
from panther.logger import logger
|
115
129
|
from panther.websocket import GenericWebsocket, Websocket
|
130
|
+
monitoring = Monitoring(is_active=config['monitoring'], is_ws=True)
|
116
131
|
|
117
132
|
temp_connection = Websocket(scope=scope, receive=receive, send=send)
|
133
|
+
await monitoring.before(request=temp_connection)
|
118
134
|
|
119
135
|
endpoint, found_path = find_endpoint(path=temp_connection.path)
|
120
136
|
if endpoint is None:
|
137
|
+
await monitoring.after('Rejected')
|
121
138
|
return await temp_connection.close(status.WS_1000_NORMAL_CLOSURE)
|
122
139
|
path_variables: dict = collect_path_variables(request_path=temp_connection.path, found_path=found_path)
|
123
140
|
|
124
141
|
if not issubclass(endpoint, GenericWebsocket):
|
125
142
|
logger.critical(f'You may have forgotten to inherit from GenericWebsocket on the {endpoint.__name__}()')
|
143
|
+
await monitoring.after('Rejected')
|
126
144
|
return await temp_connection.close(status.WS_1014_BAD_GATEWAY)
|
127
145
|
|
128
146
|
del temp_connection
|
@@ -138,24 +156,22 @@ class Panther:
|
|
138
156
|
break
|
139
157
|
else:
|
140
158
|
await self.websocket_connections.new_connection(connection=connection)
|
159
|
+
await monitoring.after('Accepted')
|
141
160
|
await connection.listen()
|
142
161
|
|
143
162
|
# Call 'After' Middleware
|
144
163
|
for middleware in config['reversed_middlewares']:
|
145
164
|
with contextlib.suppress(APIException):
|
146
165
|
await middleware.after(response=connection)
|
166
|
+
|
167
|
+
await monitoring.after('Closed')
|
147
168
|
return None
|
148
169
|
|
149
170
|
async def handle_http(self, scope: dict, receive: Callable, send: Callable) -> None:
|
150
|
-
from panther.logger import logger
|
151
|
-
|
152
171
|
request = Request(scope=scope, receive=receive, send=send)
|
153
172
|
|
154
|
-
# Monitoring
|
155
|
-
|
156
|
-
if config['monitoring']:
|
157
|
-
monitoring_middleware = MonitoringMiddleware()
|
158
|
-
await monitoring_middleware.before(request=request)
|
173
|
+
# Monitoring
|
174
|
+
await self.monitoring.before(request=request)
|
159
175
|
|
160
176
|
# Read Request Payload
|
161
177
|
await request.read_body()
|
@@ -168,7 +184,7 @@ class Panther:
|
|
168
184
|
return await http_response(
|
169
185
|
send,
|
170
186
|
status_code=status.HTTP_404_NOT_FOUND,
|
171
|
-
monitoring=
|
187
|
+
monitoring=self.monitoring,
|
172
188
|
exception=True,
|
173
189
|
)
|
174
190
|
|
@@ -185,7 +201,7 @@ class Panther:
|
|
185
201
|
return await http_response(
|
186
202
|
send,
|
187
203
|
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
188
|
-
monitoring=
|
204
|
+
monitoring=self.monitoring,
|
189
205
|
exception=True,
|
190
206
|
)
|
191
207
|
|
@@ -201,7 +217,7 @@ class Panther:
|
|
201
217
|
return await http_response(
|
202
218
|
send,
|
203
219
|
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
204
|
-
monitoring=
|
220
|
+
monitoring=self.monitoring,
|
205
221
|
exception=True,
|
206
222
|
)
|
207
223
|
# Declare Endpoint
|
@@ -220,7 +236,7 @@ class Panther:
|
|
220
236
|
return await http_response(
|
221
237
|
send,
|
222
238
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
223
|
-
monitoring=
|
239
|
+
monitoring=self.monitoring,
|
224
240
|
exception=True,
|
225
241
|
)
|
226
242
|
|
@@ -234,7 +250,7 @@ class Panther:
|
|
234
250
|
await http_response(
|
235
251
|
send,
|
236
252
|
status_code=response.status_code,
|
237
|
-
monitoring=
|
253
|
+
monitoring=self.monitoring,
|
238
254
|
headers=response.headers,
|
239
255
|
body=response.body,
|
240
256
|
)
|
panther/middlewares/redis.py
CHANGED
@@ -1,13 +1,16 @@
|
|
1
|
+
import logging
|
1
2
|
from redis import Redis
|
2
3
|
|
3
4
|
from panther.db.connection import RedisConnection
|
4
|
-
from panther.logger import logger
|
5
5
|
from panther.middlewares.base import BaseMiddleware
|
6
6
|
from panther.request import Request
|
7
7
|
from panther.response import Response
|
8
8
|
from panther.websocket import GenericWebsocket
|
9
9
|
|
10
10
|
|
11
|
+
logger = logging.getLogger('panther')
|
12
|
+
|
13
|
+
|
11
14
|
class RedisMiddleware(BaseMiddleware):
|
12
15
|
def __init__(self, **kwargs):
|
13
16
|
self.kwargs = kwargs
|
@@ -35,4 +38,6 @@ class RedisMiddleware(BaseMiddleware):
|
|
35
38
|
return response
|
36
39
|
|
37
40
|
def redis_connection_for_ws(self) -> Redis:
|
38
|
-
|
41
|
+
r = Redis(**self.kwargs)
|
42
|
+
r.ping()
|
43
|
+
return r
|
panther/monitoring.py
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
import logging
|
2
|
+
from time import perf_counter
|
3
|
+
from typing import Literal
|
4
|
+
|
5
|
+
from panther.base_request import BaseRequest
|
6
|
+
|
7
|
+
|
8
|
+
logger = logging.getLogger('monitoring')
|
9
|
+
|
10
|
+
|
11
|
+
class Monitoring:
|
12
|
+
"""
|
13
|
+
Create Log Message Like Below:
|
14
|
+
date time | method | path | ip:port | response_time [ms, s] | status
|
15
|
+
"""
|
16
|
+
def __init__(self, is_active: bool, is_ws: bool = False):
|
17
|
+
self.is_active = is_active
|
18
|
+
self.is_ws = is_ws
|
19
|
+
|
20
|
+
async def before(self, request: BaseRequest):
|
21
|
+
if self.is_active:
|
22
|
+
ip, port = request.client
|
23
|
+
|
24
|
+
if self.is_ws:
|
25
|
+
method = 'WS'
|
26
|
+
else:
|
27
|
+
method = request.scope['method']
|
28
|
+
|
29
|
+
self.log = f'{method} | {request.path} | {ip}:{port}'
|
30
|
+
self.start_time = perf_counter()
|
31
|
+
|
32
|
+
async def after(self, status: int | Literal['Accepted', 'Rejected', 'Closed'], /):
|
33
|
+
if self.is_active:
|
34
|
+
response_time = perf_counter() - self.start_time
|
35
|
+
time_unit = ' s'
|
36
|
+
|
37
|
+
if response_time < 0.01:
|
38
|
+
response_time = response_time * 1_000
|
39
|
+
time_unit = 'ms'
|
40
|
+
|
41
|
+
logger.info(f'{self.log} | {round(response_time, 4)} {time_unit} | {status}')
|
panther/panel/apis.py
CHANGED
@@ -8,16 +8,11 @@ from panther.response import Response
|
|
8
8
|
|
9
9
|
@API()
|
10
10
|
async def models_api():
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
'path': m['path'],
|
17
|
-
'index': i
|
18
|
-
}
|
19
|
-
result.append(data)
|
20
|
-
return result
|
11
|
+
return [{
|
12
|
+
'name': m['name'],
|
13
|
+
'module': m['module'],
|
14
|
+
'index': i
|
15
|
+
} for i, m in enumerate(config['models'])]
|
21
16
|
|
22
17
|
|
23
18
|
@API()
|
panther/request.py
CHANGED
@@ -1,10 +1,13 @@
|
|
1
|
+
import logging
|
1
2
|
from typing import Literal
|
2
3
|
|
3
4
|
import orjson as json
|
4
5
|
|
5
6
|
from panther._utils import read_multipart_form_data
|
6
7
|
from panther.base_request import BaseRequest
|
7
|
-
|
8
|
+
|
9
|
+
|
10
|
+
logger = logging.getLogger('panther')
|
8
11
|
|
9
12
|
|
10
13
|
class Request(BaseRequest):
|
panther/routings.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import logging
|
1
2
|
import re
|
2
3
|
from collections import Counter
|
3
4
|
from collections.abc import Callable, Mapping, MutableMapping
|
@@ -7,6 +8,9 @@ from functools import partial, reduce
|
|
7
8
|
from panther.configs import config
|
8
9
|
|
9
10
|
|
11
|
+
logger = logging.getLogger('panther')
|
12
|
+
|
13
|
+
|
10
14
|
def flatten_urls(urls: dict) -> dict:
|
11
15
|
return dict(_flattening_urls(urls))
|
12
16
|
|
@@ -29,8 +33,6 @@ def _flattening_urls(data: dict | Callable, url: str = ''):
|
|
29
33
|
|
30
34
|
|
31
35
|
def _is_url_endpoint_valid(url: str, endpoint: Callable) -> bool:
|
32
|
-
from panther.logger import logger
|
33
|
-
|
34
36
|
if endpoint is ...:
|
35
37
|
logger.error(f"URL Can't Point To Ellipsis. ('{url}' -> ...)")
|
36
38
|
elif endpoint is None:
|
panther/utils.py
CHANGED
@@ -1,9 +1,12 @@
|
|
1
|
+
import logging
|
2
|
+
import os
|
1
3
|
import hashlib
|
2
4
|
from datetime import datetime, timedelta
|
3
5
|
from pathlib import Path
|
4
6
|
from typing import ClassVar
|
5
7
|
|
6
|
-
|
8
|
+
|
9
|
+
logger = logging.getLogger('panther')
|
7
10
|
|
8
11
|
|
9
12
|
class Singleton(object):
|
@@ -30,6 +33,9 @@ def load_env(env_file: str | Path, /) -> dict[str, str]:
|
|
30
33
|
key = key.strip()
|
31
34
|
value = value.strip().strip('"\'')
|
32
35
|
variables[key] = value
|
36
|
+
|
37
|
+
# Load them as system environment variable
|
38
|
+
os.environ[key] = value
|
33
39
|
return variables
|
34
40
|
|
35
41
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: panther
|
3
|
-
Version: 3.1.
|
3
|
+
Version: 3.1.5
|
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
|
@@ -18,7 +18,7 @@ Requires-Dist: bson ~=0.5
|
|
18
18
|
Requires-Dist: httptools ~=0.6
|
19
19
|
Requires-Dist: pantherdb ~=1.3
|
20
20
|
Requires-Dist: pydantic ~=2.1
|
21
|
-
Requires-Dist: redis
|
21
|
+
Requires-Dist: redis ==5.0.1
|
22
22
|
Requires-Dist: rich ~=13.5
|
23
23
|
Requires-Dist: uvicorn ~=0.23
|
24
24
|
Requires-Dist: watchfiles ~=0.19
|
@@ -1,53 +1,53 @@
|
|
1
|
-
panther/__init__.py,sha256=
|
2
|
-
panther/_load_configs.py,sha256=
|
3
|
-
panther/_utils.py,sha256=
|
4
|
-
panther/app.py,sha256=
|
5
|
-
panther/authentications.py,sha256=
|
6
|
-
panther/background_tasks.py,sha256=
|
7
|
-
panther/base_request.py,sha256=
|
8
|
-
panther/base_websocket.py,sha256=
|
9
|
-
panther/caching.py,sha256=
|
10
|
-
panther/configs.py,sha256=
|
1
|
+
panther/__init__.py,sha256=6LwXZVmGyh5YmGDfYREWyRqoMsv9NiCxNibakKFUzJo,110
|
2
|
+
panther/_load_configs.py,sha256=S5zN5ErktjXCIi7cgg8HZwRCez51qiYxa1Ppu2No9Xw,7476
|
3
|
+
panther/_utils.py,sha256=H0VIv9VPt7mIDZo-N7x5ZaMMes1jo60wp1TtxkNvFnQ,4346
|
4
|
+
panther/app.py,sha256=8-W1XsOfVjNcP1RxV8X9TgQzshP9Qk9E0LBcXBf25aM,7961
|
5
|
+
panther/authentications.py,sha256=Fh7Z7rpysSJXN9xEKJqH94oYf2L3jJQxr6fPhRz3UFg,4149
|
6
|
+
panther/background_tasks.py,sha256=J6JrjiEzyGohYsaFZHnZtXeP52piP84RlGFLsKOFa-Y,7225
|
7
|
+
panther/base_request.py,sha256=GhKfPdDDl-r9l9ro8DWnUNtC_10Nh1EhVy8lX4ULLhs,2751
|
8
|
+
panther/base_websocket.py,sha256=aaonC-7EAdA9fkzVQobTS-a-tTvdjI6cnMSMIMUdW6Q,6161
|
9
|
+
panther/caching.py,sha256=Kwhhv0fqCRWbYiq12jwiTdedSSsTy3z_NTBjWdUPgnA,2553
|
10
|
+
panther/configs.py,sha256=lvmPDT_W8gnXNv214zLQedYFZhM78E-3XGa7KyWQbo4,1920
|
11
11
|
panther/exceptions.py,sha256=DB6nGfU5LxrglEN_I-HInMqdIA3ZmN8rRv0ynEuQyGA,1332
|
12
12
|
panther/file_handler.py,sha256=b-paQykGfjlvslfEkM23V50v8bosjRLq1S1HmsmlXEc,855
|
13
|
-
panther/
|
14
|
-
panther/main.py,sha256=
|
13
|
+
panther/logging.py,sha256=DZVf3nxzLodT-hD4820J1jEAffU8zIxXRPKs2lbP8ho,2074
|
14
|
+
panther/main.py,sha256=F8yGaVN97JMD0FKWvTPuGZ43g1BGdEEtDzjWSelG4mg,10301
|
15
|
+
panther/monitoring.py,sha256=krmcoTUcV12pHwCFVAHywgsp1-US9cyiMlgsrMJdUQ0,1203
|
15
16
|
panther/permissions.py,sha256=Q-l25369yQaP-tY11tFQm-v9PPh8iVImbfUpY3pnQUk,355
|
16
|
-
panther/request.py,sha256=
|
17
|
+
panther/request.py,sha256=W3L_udvIxgjqVxxc8aaKDIQBVjdc7TTrFyoOJQ8M8FA,1773
|
17
18
|
panther/response.py,sha256=HcL8nuXpZX_vhT9QUw0dnbtP75T_LbvKAWGT48_LPuU,3382
|
18
|
-
panther/routings.py,sha256=
|
19
|
+
panther/routings.py,sha256=Y5FH5Of7zf--mhjFt26G6bTKcTZyL5XkQoVLqAHxfGM,5140
|
19
20
|
panther/status.py,sha256=Gc_PnYrHfInTsZpGbqiCfDB-py1C7Rh8KMdb6Lq9Exs,3346
|
20
21
|
panther/test.py,sha256=withvHE4Vsv7hakPMfadoKBP59jB0-CMzMNBgEuiCbM,4748
|
21
22
|
panther/throttling.py,sha256=mVa_mGv6w_Ad7LLtV4eG5QpDwwNsk4QjFFi0mIHQBnE,231
|
22
|
-
panther/utils.py,sha256=
|
23
|
+
panther/utils.py,sha256=6MjJSJgWeYn2rWMlqS4_kwyhfipJPWser03JAVBy-_s,1752
|
23
24
|
panther/websocket.py,sha256=KKnSqgnaP5-BsA87qUM7Y176ECWSnBWZ5ENbJBo8BiU,2167
|
24
25
|
panther/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
25
26
|
panther/cli/create_command.py,sha256=1OFNE9pk2mhXNQMzjdDEtp0RocHy0u6kyhrhlLso1DM,2932
|
26
27
|
panther/cli/main.py,sha256=84k70hh9BXU5fegNR44JZN7eH7AOz4bXotRJLbNQ55w,1894
|
27
|
-
panther/cli/monitor_command.py,sha256=
|
28
|
+
panther/cli/monitor_command.py,sha256=cZ7wOxeQ8jKXgRLP28SgFI9pbIEp4RK6VmVf74WKT6c,2447
|
28
29
|
panther/cli/run_command.py,sha256=X8DTHPyfpC7h9HNpVDhpjUN52buauEgAwxlCV0MIloQ,2020
|
29
30
|
panther/cli/template.py,sha256=kXLsT2YANV0pw1L82H3epgAaICcy3SuHmH8QuAxnxHQ,3044
|
30
|
-
panther/cli/utils.py,sha256=
|
31
|
+
panther/cli/utils.py,sha256=fOLlUAUqQ8_-GP_23RhgXjyDpIS260LazIYs8W9mNOk,3637
|
31
32
|
panther/db/__init__.py,sha256=w9lEL0vRqb18Qx_iUJipUR_fi5GQ5uVX0DWycx14x08,50
|
32
33
|
panther/db/connection.py,sha256=UwssfAXwCRNveT_dMr51n9Pu9AdMcC4IYuv8aT5Y27c,2323
|
33
34
|
panther/db/models.py,sha256=VytEvIRgI12Co3WyDsN8lycf_AWMRdOCZt4JOa1P_n0,1464
|
34
|
-
panther/db/utils.py,sha256=
|
35
|
+
panther/db/utils.py,sha256=G1TcS9cBAA7AmIBNvXRK-4DRsnho1zYYhZ5ogP9IBvQ,1822
|
35
36
|
panther/db/queries/__init__.py,sha256=FpSQGNHGMs5PJow8Qan4eBAld6QH6wfMvj7lC92vKcU,55
|
36
37
|
panther/db/queries/mongodb_queries.py,sha256=17yq8J7m5_312Eb4XYitE3tAcfUjGgPMGBhRa4eB7K4,3358
|
37
38
|
panther/db/queries/pantherdb_queries.py,sha256=giFLUxjmQykuZOySlBxQa5z6ex49H_8ZrKWzOno1NpU,3269
|
38
|
-
panther/db/queries/queries.py,sha256=
|
39
|
+
panther/db/queries/queries.py,sha256=5DAaTCLu-SfWHjJUnaLcr6HqSn6fAe11fjgB-7BqGMU,7598
|
39
40
|
panther/middlewares/__init__.py,sha256=ydo0bSadGqa2v7Xy1oCTkF2uXrImedXjiyx2vPTwPhE,66
|
40
41
|
panther/middlewares/base.py,sha256=kBJp0U6982pfwDRSzDfMRfdUra9oVoL9Ms52EavW0r4,239
|
41
42
|
panther/middlewares/db.py,sha256=Dotc7LPKEZ26w3yK0SY_PhO47JEKJtYpVZPO9SLA2Lg,638
|
42
|
-
panther/middlewares/
|
43
|
-
panther/middlewares/redis.py,sha256=A1JuYnCndwekoq7PC9N5ujZUKkqZklU4HLke5t10usU,1322
|
43
|
+
panther/middlewares/redis.py,sha256=Wa_uvTe2yGw9LYO1CehrUYYXc6hdyNMOJc7X5lDJ5LU,1374
|
44
44
|
panther/panel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
45
|
-
panther/panel/apis.py,sha256=
|
45
|
+
panther/panel/apis.py,sha256=f4eWTi212ZnnPYcxuU4SdJ-7yYQxZFImaXgTTnXw_fc,1703
|
46
46
|
panther/panel/urls.py,sha256=BQkWqSJBPP3VEQYeorKSHIRx-PUl21Y7Z6NFylmhs1I,192
|
47
47
|
panther/panel/utils.py,sha256=0Rv79oR5IEqalqwpRKQHMn1p5duVY5mxMqDKiA5mWx4,437
|
48
|
-
panther-3.1.
|
49
|
-
panther-3.1.
|
50
|
-
panther-3.1.
|
51
|
-
panther-3.1.
|
52
|
-
panther-3.1.
|
53
|
-
panther-3.1.
|
48
|
+
panther-3.1.5.dist-info/LICENSE,sha256=2aF1hL2aC0zRPjzUkSxJUzZbn2_uLoOkn7DHjzZni-I,1524
|
49
|
+
panther-3.1.5.dist-info/METADATA,sha256=YmW-daTXDu085RPgfX1YbQYKkq6SewmDkw4QR50E-eY,6076
|
50
|
+
panther-3.1.5.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
51
|
+
panther-3.1.5.dist-info/entry_points.txt,sha256=6GPxYFGuzVfNB4YpHFJvYex6iWah5_tLnirAHwj2Qsg,51
|
52
|
+
panther-3.1.5.dist-info/top_level.txt,sha256=VbBs02JGXTIoHMzsX-eLOk2MCbBZzQbLhWiYpI7xI2g,8
|
53
|
+
panther-3.1.5.dist-info/RECORD,,
|
panther/logger.py
DELETED
@@ -1,89 +0,0 @@
|
|
1
|
-
import logging
|
2
|
-
from logging.config import dictConfig
|
3
|
-
from pathlib import Path
|
4
|
-
|
5
|
-
from pydantic import BaseModel
|
6
|
-
|
7
|
-
from panther.configs import config
|
8
|
-
|
9
|
-
LOGS_DIR = config['base_dir'] / 'logs'
|
10
|
-
|
11
|
-
|
12
|
-
class LogConfig(BaseModel):
|
13
|
-
"""Logging configuration to be set for the server."""
|
14
|
-
|
15
|
-
LOGGER_NAME: str = 'panther-logger'
|
16
|
-
DEFAULT_LOG_FORMAT: str = '%(levelprefix)s | %(asctime)s | %(message)s'
|
17
|
-
FILE_LOG_FORMAT: str = '%(asctime)s | %(message)s'
|
18
|
-
LOG_LEVEL: str = 'DEBUG'
|
19
|
-
MAX_FILE_SIZE: int = 1024 * 1024 * 100 # 100 MB
|
20
|
-
|
21
|
-
version: int = 1
|
22
|
-
disable_existing_loggers: bool = False
|
23
|
-
|
24
|
-
formatters: dict = {
|
25
|
-
'default': {
|
26
|
-
'()': 'uvicorn.logging.DefaultFormatter',
|
27
|
-
'fmt': DEFAULT_LOG_FORMAT,
|
28
|
-
'datefmt': '%Y-%m-%d %H:%M:%S',
|
29
|
-
},
|
30
|
-
'file_formatter': {
|
31
|
-
'()': 'uvicorn.logging.DefaultFormatter',
|
32
|
-
'fmt': FILE_LOG_FORMAT,
|
33
|
-
'datefmt': '%Y-%m-%d %H:%M:%S',
|
34
|
-
},
|
35
|
-
}
|
36
|
-
handlers: dict = {
|
37
|
-
'monitoring_file': {
|
38
|
-
'formatter': 'file_formatter',
|
39
|
-
'filename': LOGS_DIR / 'monitoring.log',
|
40
|
-
'class': 'logging.handlers.RotatingFileHandler',
|
41
|
-
'maxBytes': MAX_FILE_SIZE, # 100 MB,
|
42
|
-
'backupCount': 3,
|
43
|
-
},
|
44
|
-
'query_file': {
|
45
|
-
'formatter': 'file_formatter',
|
46
|
-
'filename': LOGS_DIR / 'query.log',
|
47
|
-
'class': 'logging.handlers.RotatingFileHandler',
|
48
|
-
'maxBytes': MAX_FILE_SIZE, # 100 MB,
|
49
|
-
'backupCount': 3,
|
50
|
-
},
|
51
|
-
'file': {
|
52
|
-
'formatter': 'file_formatter',
|
53
|
-
'filename': LOGS_DIR / 'main.log',
|
54
|
-
'class': 'logging.handlers.RotatingFileHandler',
|
55
|
-
'maxBytes': MAX_FILE_SIZE, # 100 MB,
|
56
|
-
'backupCount': 3,
|
57
|
-
},
|
58
|
-
'default': {
|
59
|
-
'formatter': 'default',
|
60
|
-
'class': 'logging.StreamHandler',
|
61
|
-
'stream': 'ext://sys.stderr',
|
62
|
-
},
|
63
|
-
}
|
64
|
-
loggers: dict = {
|
65
|
-
'panther': {
|
66
|
-
'handlers': ['default', 'file'],
|
67
|
-
'level': LOG_LEVEL,
|
68
|
-
},
|
69
|
-
'monitoring': {
|
70
|
-
'handlers': ['monitoring_file'],
|
71
|
-
'level': LOG_LEVEL,
|
72
|
-
},
|
73
|
-
'query': {
|
74
|
-
'handlers': ['default', 'query_file'],
|
75
|
-
'level': LOG_LEVEL,
|
76
|
-
},
|
77
|
-
}
|
78
|
-
|
79
|
-
|
80
|
-
try:
|
81
|
-
dictConfig(LogConfig().model_dump())
|
82
|
-
except ValueError:
|
83
|
-
LOGS_DIR = config['base_dir'] / 'logs'
|
84
|
-
Path(LOGS_DIR).mkdir(exist_ok=True)
|
85
|
-
|
86
|
-
|
87
|
-
logger = logging.getLogger('panther')
|
88
|
-
query_logger = logging.getLogger('query')
|
89
|
-
monitoring_logger = logging.getLogger('monitoring')
|
@@ -1,26 +0,0 @@
|
|
1
|
-
from time import perf_counter
|
2
|
-
|
3
|
-
from panther.logger import monitoring_logger
|
4
|
-
from panther.middlewares.base import BaseMiddleware
|
5
|
-
from panther.request import Request
|
6
|
-
|
7
|
-
|
8
|
-
class Middleware(BaseMiddleware):
|
9
|
-
"""
|
10
|
-
Create Log Message Like Below:
|
11
|
-
[method] path | ip:port | response_time ms | status_code
|
12
|
-
"""
|
13
|
-
|
14
|
-
async def before(self, request: Request) -> Request:
|
15
|
-
ip, port = request.client
|
16
|
-
self.log = f'{request.method} | {request.path} | {ip}:{port}'
|
17
|
-
self.start_time = perf_counter()
|
18
|
-
return request
|
19
|
-
|
20
|
-
async def after(self, status_code: int):
|
21
|
-
"""
|
22
|
-
We handled Monitoring Middle manually,
|
23
|
-
cause of that we only have "status_code" here
|
24
|
-
"""
|
25
|
-
response_time = (perf_counter() - self.start_time) * 1_000
|
26
|
-
monitoring_logger.info(f'{self.log} | {response_time: .3} ms | {status_code}')
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|