panther 5.0.0b3__py3-none-any.whl → 5.0.0b4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- panther/__init__.py +1 -1
- panther/_load_configs.py +46 -37
- panther/_utils.py +49 -34
- panther/app.py +96 -97
- panther/authentications.py +97 -50
- panther/background_tasks.py +98 -124
- panther/base_request.py +16 -10
- panther/base_websocket.py +8 -8
- panther/caching.py +16 -80
- panther/cli/create_command.py +17 -16
- panther/cli/main.py +1 -1
- panther/cli/monitor_command.py +11 -6
- panther/cli/run_command.py +5 -71
- panther/cli/template.py +7 -7
- panther/cli/utils.py +58 -69
- panther/configs.py +70 -72
- panther/db/connections.py +18 -24
- panther/db/cursor.py +0 -1
- panther/db/models.py +24 -8
- panther/db/queries/base_queries.py +2 -5
- panther/db/queries/mongodb_queries.py +17 -20
- panther/db/queries/pantherdb_queries.py +1 -1
- panther/db/queries/queries.py +26 -8
- panther/db/utils.py +1 -1
- panther/events.py +25 -14
- panther/exceptions.py +2 -7
- panther/file_handler.py +1 -1
- panther/generics.py +11 -8
- panther/logging.py +2 -1
- panther/main.py +12 -13
- panther/middlewares/cors.py +67 -0
- panther/middlewares/monitoring.py +5 -3
- panther/openapi/urls.py +2 -2
- panther/openapi/utils.py +3 -3
- panther/openapi/views.py +20 -37
- panther/pagination.py +4 -2
- panther/panel/apis.py +2 -7
- panther/panel/urls.py +2 -6
- panther/panel/utils.py +9 -5
- panther/panel/views.py +13 -22
- panther/permissions.py +2 -1
- panther/request.py +2 -1
- panther/response.py +53 -47
- panther/routings.py +12 -12
- panther/serializer.py +19 -20
- panther/test.py +73 -58
- panther/throttling.py +68 -3
- panther/utils.py +5 -11
- {panther-5.0.0b3.dist-info → panther-5.0.0b4.dist-info}/METADATA +1 -1
- panther-5.0.0b4.dist-info/RECORD +75 -0
- panther/monitoring.py +0 -34
- panther-5.0.0b3.dist-info/RECORD +0 -75
- {panther-5.0.0b3.dist-info → panther-5.0.0b4.dist-info}/WHEEL +0 -0
- {panther-5.0.0b3.dist-info → panther-5.0.0b4.dist-info}/entry_points.txt +0 -0
- {panther-5.0.0b3.dist-info → panther-5.0.0b4.dist-info}/licenses/LICENSE +0 -0
- {panther-5.0.0b3.dist-info → panther-5.0.0b4.dist-info}/top_level.txt +0 -0
panther/base_websocket.py
CHANGED
@@ -12,7 +12,7 @@ from panther import status
|
|
12
12
|
from panther.base_request import BaseRequest
|
13
13
|
from panther.configs import config
|
14
14
|
from panther.db.connections import redis
|
15
|
-
from panther.exceptions import
|
15
|
+
from panther.exceptions import BaseError, InvalidPathVariableAPIError
|
16
16
|
from panther.utils import Singleton
|
17
17
|
|
18
18
|
if TYPE_CHECKING:
|
@@ -83,11 +83,11 @@ class WebsocketConnections(Singleton):
|
|
83
83
|
|
84
84
|
async def _handle_received_message(self, received_message):
|
85
85
|
if (
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
86
|
+
isinstance(received_message, dict)
|
87
|
+
and (connection_id := received_message.get('connection_id'))
|
88
|
+
and connection_id in self.connections
|
89
|
+
and 'action' in received_message
|
90
|
+
and 'data' in received_message
|
91
91
|
):
|
92
92
|
# Check Action of WS
|
93
93
|
match received_message['action']:
|
@@ -96,7 +96,7 @@ class WebsocketConnections(Singleton):
|
|
96
96
|
case 'close':
|
97
97
|
await self.connections[connection_id].close(
|
98
98
|
code=received_message['data']['code'],
|
99
|
-
reason=received_message['data']['reason']
|
99
|
+
reason=received_message['data']['reason'],
|
100
100
|
)
|
101
101
|
case unknown_action:
|
102
102
|
logger.error(f'Unknown Message Action: {unknown_action}')
|
@@ -124,7 +124,7 @@ class WebsocketConnections(Singleton):
|
|
124
124
|
|
125
125
|
# 3. Put PathVariables and Request(If User Wants It) In kwargs
|
126
126
|
try:
|
127
|
-
kwargs = connection.clean_parameters(connection.connect)
|
127
|
+
kwargs = connection.clean_parameters(connection.connect.__annotations__)
|
128
128
|
except InvalidPathVariableAPIError as e:
|
129
129
|
connection.change_state(state='Rejected', message=e.detail)
|
130
130
|
return await connection.close()
|
panther/caching.py
CHANGED
@@ -1,42 +1,33 @@
|
|
1
|
-
from collections import namedtuple
|
2
|
-
from datetime import timedelta, datetime
|
3
1
|
import logging
|
4
|
-
from
|
2
|
+
from collections import namedtuple
|
3
|
+
from datetime import datetime, timedelta
|
5
4
|
|
6
5
|
import orjson as json
|
7
6
|
|
8
|
-
from panther.configs import config
|
9
7
|
from panther.db.connections import redis
|
10
8
|
from panther.request import Request
|
11
9
|
from panther.response import Response
|
12
|
-
from panther.throttling import throttling_storage
|
13
10
|
from panther.utils import generate_hash_value_from_string, round_datetime
|
14
11
|
|
15
12
|
logger = logging.getLogger('panther')
|
16
13
|
|
17
|
-
caches = {}
|
14
|
+
caches: dict[str, tuple[bytes, dict, int]] = {}
|
18
15
|
CachedResponse = namedtuple('CachedResponse', ['data', 'headers', 'status_code'])
|
19
16
|
|
20
17
|
|
21
|
-
def api_cache_key(request: Request,
|
22
|
-
client = request.user and request.user.id or request.client.ip
|
18
|
+
def api_cache_key(request: Request, duration: timedelta | None = None) -> str:
|
19
|
+
client = (request.user and request.user.id) or request.client.ip
|
23
20
|
query_params_hash = generate_hash_value_from_string(request.scope['query_string'].decode('utf-8'))
|
24
21
|
key = f'{client}-{request.path}-{query_params_hash}-{request.validated_data}'
|
25
22
|
|
26
|
-
if
|
27
|
-
time = round_datetime(datetime.now(),
|
23
|
+
if duration:
|
24
|
+
time = round_datetime(datetime.now(), duration)
|
28
25
|
return f'{time}-{key}'
|
29
26
|
|
30
27
|
return key
|
31
28
|
|
32
29
|
|
33
|
-
def
|
34
|
-
client = request.user and request.user.id or request.client.ip
|
35
|
-
time = round_datetime(datetime.now(), duration)
|
36
|
-
return f'{time}-{client}-{request.path}'
|
37
|
-
|
38
|
-
|
39
|
-
async def get_response_from_cache(*, request: Request, cache_exp_time: timedelta) -> CachedResponse | None:
|
30
|
+
async def get_response_from_cache(*, request: Request, duration: timedelta) -> CachedResponse | None:
|
40
31
|
"""
|
41
32
|
If redis.is_connected:
|
42
33
|
Get Cached Data From Redis
|
@@ -47,18 +38,14 @@ async def get_response_from_cache(*, request: Request, cache_exp_time: timedelta
|
|
47
38
|
key = api_cache_key(request=request)
|
48
39
|
data = (await redis.get(key) or b'{}').decode()
|
49
40
|
if value := json.loads(data):
|
50
|
-
return CachedResponse(
|
51
|
-
data=value[0].encode(),
|
52
|
-
headers=value[1],
|
53
|
-
status_code=value[2]
|
54
|
-
)
|
41
|
+
return CachedResponse(data=value[0].encode(), headers=value[1], status_code=value[2])
|
55
42
|
else:
|
56
|
-
key = api_cache_key(request=request,
|
43
|
+
key = api_cache_key(request=request, duration=duration)
|
57
44
|
if value := caches.get(key):
|
58
45
|
return CachedResponse(*value)
|
59
46
|
|
60
47
|
|
61
|
-
async def set_response_in_cache(*, request: Request, response: Response,
|
48
|
+
async def set_response_in_cache(*, request: Request, response: Response, duration: timedelta | int) -> None:
|
62
49
|
"""
|
63
50
|
If redis.is_connected:
|
64
51
|
Cache The Data In Redis
|
@@ -68,61 +55,10 @@ async def set_response_in_cache(*, request: Request, response: Response, cache_e
|
|
68
55
|
|
69
56
|
if redis.is_connected:
|
70
57
|
key = api_cache_key(request=request)
|
71
|
-
cache_data: tuple[str,
|
72
|
-
|
73
|
-
cache_data: bytes = json.dumps(cache_data)
|
74
|
-
|
75
|
-
if not isinstance(cache_exp_time, timedelta | int | NoneType):
|
76
|
-
msg = '`cache_exp_time` should be instance of `datetime.timedelta`, `int` or `None`'
|
77
|
-
raise TypeError(msg)
|
78
|
-
|
79
|
-
if cache_exp_time is None:
|
80
|
-
logger.warning(
|
81
|
-
'your response are going to cache in redis forever '
|
82
|
-
'** set DEFAULT_CACHE_EXP in `configs` or set the `cache_exp_time` in `@API.get()` to prevent this **'
|
83
|
-
)
|
84
|
-
await redis.set(key, cache_data)
|
85
|
-
else:
|
86
|
-
await redis.set(key, cache_data, ex=cache_exp_time)
|
87
|
-
|
88
|
-
else:
|
89
|
-
key = api_cache_key(request=request, cache_exp_time=cache_exp_time)
|
90
|
-
cache_data: tuple[bytes, str, int] = (response.body, response.headers, response.status_code)
|
91
|
-
|
92
|
-
caches[key] = cache_data
|
93
|
-
|
94
|
-
if cache_exp_time:
|
95
|
-
logger.info('`cache_exp_time` is not very accurate when `redis` is not connected.')
|
96
|
-
|
97
|
-
|
98
|
-
async def get_throttling_from_cache(request: Request, duration: timedelta) -> int:
|
99
|
-
"""
|
100
|
-
If redis.is_connected:
|
101
|
-
Get Cached Data From Redis
|
102
|
-
else:
|
103
|
-
Get Cached Data From Memory
|
104
|
-
"""
|
105
|
-
key = throttling_cache_key(request=request, duration=duration)
|
106
|
-
|
107
|
-
if redis.is_connected:
|
108
|
-
data = (await redis.get(key) or b'0').decode()
|
109
|
-
return json.loads(data)
|
110
|
-
|
111
|
-
else:
|
112
|
-
return throttling_storage[key]
|
113
|
-
|
114
|
-
|
115
|
-
async def increment_throttling_in_cache(request: Request, duration: timedelta) -> None:
|
116
|
-
"""
|
117
|
-
If redis.is_connected:
|
118
|
-
Increment The Data In Redis
|
119
|
-
else:
|
120
|
-
Increment The Data In Memory
|
121
|
-
"""
|
122
|
-
key = throttling_cache_key(request=request, duration=duration)
|
123
|
-
|
124
|
-
if redis.is_connected:
|
125
|
-
await redis.incrby(key, amount=1)
|
58
|
+
cache_data: tuple[str, dict, int] = (response.body.decode(), response.headers, response.status_code)
|
59
|
+
await redis.set(key, json.dumps(cache_data), ex=duration)
|
126
60
|
|
127
61
|
else:
|
128
|
-
|
62
|
+
key = api_cache_key(request=request, duration=duration)
|
63
|
+
caches[key] = (response.body, response.headers, response.status_code)
|
64
|
+
logger.info('`cache` is not very accurate when `redis` is not connected.')
|
panther/cli/create_command.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
+
from collections.abc import Callable
|
1
2
|
from pathlib import Path
|
2
|
-
from typing import Callable
|
3
3
|
|
4
4
|
from rich import print as rich_print
|
5
5
|
from rich.console import Console
|
@@ -8,15 +8,16 @@ from rich.prompt import Prompt
|
|
8
8
|
|
9
9
|
from panther import version
|
10
10
|
from panther.cli.template import (
|
11
|
-
TEMPLATE,
|
12
|
-
SINGLE_FILE_TEMPLATE,
|
13
11
|
AUTHENTICATION_PART,
|
14
|
-
MONITORING_PART,
|
15
|
-
LOG_QUERIES_PART,
|
16
12
|
AUTO_REFORMAT_PART,
|
17
|
-
DATABASE_PANTHERDB_PART,
|
18
13
|
DATABASE_MONGODB_PART,
|
19
|
-
|
14
|
+
DATABASE_PANTHERDB_PART,
|
15
|
+
LOG_QUERIES_PART,
|
16
|
+
MONITORING_PART,
|
17
|
+
REDIS_PART,
|
18
|
+
SINGLE_FILE_TEMPLATE,
|
19
|
+
TEMPLATE,
|
20
|
+
USER_MODEL_PART,
|
20
21
|
)
|
21
22
|
from panther.cli.utils import cli_error
|
22
23
|
|
@@ -52,7 +53,7 @@ class CreateProject:
|
|
52
53
|
'message': 'Directory (default is .)',
|
53
54
|
'validation_func': self._check_all_directories,
|
54
55
|
'error_message': '"{}" Directory Already Exists.',
|
55
|
-
'show_validation_error': True
|
56
|
+
'show_validation_error': True,
|
56
57
|
},
|
57
58
|
{
|
58
59
|
'field': 'single_file',
|
@@ -69,7 +70,7 @@ class CreateProject:
|
|
69
70
|
'field': 'database_encryption',
|
70
71
|
'message': 'Do You Want Encryption For Your Database (Required `cryptography`)',
|
71
72
|
'is_boolean': True,
|
72
|
-
'condition': "self.database == '0'"
|
73
|
+
'condition': "self.database == '0'",
|
73
74
|
},
|
74
75
|
{
|
75
76
|
'field': 'redis',
|
@@ -90,7 +91,7 @@ class CreateProject:
|
|
90
91
|
'field': 'log_queries',
|
91
92
|
'message': 'Do You Want To Log Queries',
|
92
93
|
'is_boolean': True,
|
93
|
-
'condition': "self.database != '2'"
|
94
|
+
'condition': "self.database != '2'",
|
94
95
|
},
|
95
96
|
{
|
96
97
|
'field': 'auto_reformat',
|
@@ -194,12 +195,12 @@ class CreateProject:
|
|
194
195
|
self.progress(i + 1)
|
195
196
|
|
196
197
|
def ask(
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
198
|
+
self,
|
199
|
+
message: str,
|
200
|
+
default: str | bool,
|
201
|
+
error_message: str,
|
202
|
+
validation_func: Callable,
|
203
|
+
show_validation_error: bool = False,
|
203
204
|
) -> str:
|
204
205
|
value = Prompt.ask(message, console=self.input_console).lower() or default
|
205
206
|
while not validation_func(value):
|
panther/cli/main.py
CHANGED
@@ -15,7 +15,7 @@ def shell(args) -> None:
|
|
15
15
|
return cli_error(
|
16
16
|
'Not Enough Arguments, Give me a file path that contains `Panther()` app.\n'
|
17
17
|
' * Make sure to run `panther shell` in the same directory as that file!\n'
|
18
|
-
' * Example: `panther shell main.py`'
|
18
|
+
' * Example: `panther shell main.py`',
|
19
19
|
)
|
20
20
|
elif len(args) != 1:
|
21
21
|
return cli_error('Too Many Arguments.')
|
panther/cli/monitor_command.py
CHANGED
@@ -41,7 +41,7 @@ class Monitoring:
|
|
41
41
|
vertical_overflow='visible',
|
42
42
|
screen=True,
|
43
43
|
) as live,
|
44
|
-
contextlib.suppress(KeyboardInterrupt)
|
44
|
+
contextlib.suppress(KeyboardInterrupt),
|
45
45
|
):
|
46
46
|
f.readlines() # Set cursor at the end of the file
|
47
47
|
|
@@ -52,11 +52,12 @@ class Monitoring:
|
|
52
52
|
|
53
53
|
for _ in watching:
|
54
54
|
for line in f.readlines():
|
55
|
-
# line =
|
55
|
+
# line = datetime | method | path | ip:port | response_time(seconds) | status
|
56
56
|
columns = line.split('|')
|
57
|
-
columns
|
58
|
-
|
59
|
-
|
57
|
+
if len(columns) != 2: # Can be `datetime | ` on initiation.
|
58
|
+
columns[4] = self._clean_response_time(columns[4])
|
59
|
+
self.rows.append(columns)
|
60
|
+
live.update(self.generate_table())
|
60
61
|
|
61
62
|
def initialize(self) -> str:
|
62
63
|
# Check requirements
|
@@ -67,7 +68,11 @@ class Monitoring:
|
|
67
68
|
|
68
69
|
# Check log file
|
69
70
|
if not self.monitoring_log_file.exists():
|
70
|
-
return
|
71
|
+
return (
|
72
|
+
f'`{self.monitoring_log_file}` file not found. '
|
73
|
+
f'Make sure `panther.middlewares.monitoring.MonitoringMiddleware` is in your `MIDDLEWARES`.\n'
|
74
|
+
f'documentation: https://PantherPy.github.io/middlewares/#monitoring-middleware'
|
75
|
+
)
|
71
76
|
|
72
77
|
# Initialize Deque
|
73
78
|
self.update_rows()
|
panther/cli/run_command.py
CHANGED
@@ -1,82 +1,16 @@
|
|
1
1
|
import contextlib
|
2
|
-
import
|
2
|
+
import sys
|
3
3
|
|
4
4
|
import uvicorn
|
5
5
|
|
6
|
-
from panther.cli.utils import
|
7
|
-
|
8
|
-
|
9
|
-
def _handle_commands(args: dict[str, str | None]) -> dict:
|
10
|
-
"""
|
11
|
-
Boolean Commands:
|
12
|
-
- reload
|
13
|
-
- access-log
|
14
|
-
- no-access-log
|
15
|
-
- use-colors
|
16
|
-
- no-use-colors
|
17
|
-
- server-header
|
18
|
-
- no-server-header
|
19
|
-
|
20
|
-
Int Commands:
|
21
|
-
- port
|
22
|
-
- ws_max_size
|
23
|
-
- ws_max_queue
|
24
|
-
- ws_ping_interval
|
25
|
-
- ws_ping_timeout
|
26
|
-
"""
|
27
|
-
_command = {}
|
28
|
-
|
29
|
-
if 'reload' in args:
|
30
|
-
_command['reload'] = bool(args.pop('reload', None))
|
31
|
-
|
32
|
-
_command['access_log'] = False # Default
|
33
|
-
if 'access-log' in args:
|
34
|
-
_command['access_log'] = bool(args.pop('access-log', None))
|
35
|
-
|
36
|
-
if 'no-access-log' in args:
|
37
|
-
_command['access_log'] = not bool(args.pop('no-access-log', None))
|
38
|
-
|
39
|
-
if 'use-colors' in args:
|
40
|
-
_command['use_colors'] = bool(args.pop('use-colors', None))
|
41
|
-
|
42
|
-
if 'no-use-colors' in args:
|
43
|
-
_command['use_colors'] = not bool(args.pop('no-use-colors', None))
|
44
|
-
|
45
|
-
if 'server-header' in args:
|
46
|
-
_command['server_header'] = bool(args.pop('server-header', None))
|
47
|
-
|
48
|
-
if 'no-server-header' in args:
|
49
|
-
_command['server_header'] = not bool(args.pop('no-server-header', None))
|
50
|
-
|
51
|
-
if 'port' in args:
|
52
|
-
_command['port'] = int(args.pop('port'))
|
53
|
-
|
54
|
-
if 'ws_max_size' in args:
|
55
|
-
_command['ws_max_size'] = int(args.pop('ws_max_size'))
|
56
|
-
|
57
|
-
if 'ws_max_queue' in args:
|
58
|
-
_command['ws_max_queue'] = int(args.pop('ws_max_queue'))
|
59
|
-
|
60
|
-
if 'ws_ping_interval' in args:
|
61
|
-
_command['ws_ping_interval'] = int(args.pop('ws_ping_interval'))
|
62
|
-
|
63
|
-
if 'ws_ping_timeout' in args:
|
64
|
-
_command['ws_ping_timeout'] = int(args.pop('ws_ping_timeout'))
|
65
|
-
|
66
|
-
return _command
|
6
|
+
from panther.cli.utils import cli_error
|
67
7
|
|
68
8
|
|
69
9
|
def run(args: list[str]) -> None:
|
70
|
-
args = clean_args(args)
|
71
|
-
|
72
|
-
if any(a in args for a in ['h', 'help', '-h', '--help']):
|
73
|
-
print_uvicorn_help_message()
|
74
|
-
return
|
75
|
-
command = {'app_dir': os.getcwd()}
|
76
|
-
command.update(_handle_commands(args))
|
77
|
-
command.update(args)
|
78
10
|
try:
|
79
11
|
with contextlib.suppress(KeyboardInterrupt):
|
80
|
-
|
12
|
+
# First arg will be ignored by @Click, so ...
|
13
|
+
sys.argv = ['main'] + args
|
14
|
+
uvicorn.main()
|
81
15
|
except TypeError as e:
|
82
16
|
cli_error(e)
|
panther/cli/template.py
CHANGED
@@ -5,7 +5,7 @@ from panther.utils import generate_secret_key
|
|
5
5
|
|
6
6
|
apis_py = """from datetime import datetime
|
7
7
|
|
8
|
-
from app.throttling import
|
8
|
+
from app.throttling import InfoThrottle
|
9
9
|
|
10
10
|
from panther import status, version
|
11
11
|
from panther.app import API
|
@@ -19,7 +19,7 @@ async def hello_world_api():
|
|
19
19
|
return {'detail': 'Hello World'}
|
20
20
|
|
21
21
|
|
22
|
-
@API(cache=True, throttling=
|
22
|
+
@API(cache=True, throttling=InfoThrottle)
|
23
23
|
async def info_api(request: Request):
|
24
24
|
data = {
|
25
25
|
'panther_version': version(),
|
@@ -39,9 +39,9 @@ serializers_py = """from panther.serializer import ModelSerializer
|
|
39
39
|
|
40
40
|
throttling_py = """from datetime import timedelta
|
41
41
|
|
42
|
-
from panther.throttling import
|
42
|
+
from panther.throttling import Throttle
|
43
43
|
|
44
|
-
|
44
|
+
InfoThrottle = Throttle(rate=5, duration=timedelta(minutes=1))
|
45
45
|
"""
|
46
46
|
|
47
47
|
app_urls_py = """from app.apis import hello_world_api, info_api
|
@@ -132,7 +132,7 @@ from panther import Panther, status, version
|
|
132
132
|
from panther.app import API
|
133
133
|
from panther.request import Request
|
134
134
|
from panther.response import Response
|
135
|
-
from panther.throttling import
|
135
|
+
from panther.throttling import Throttle
|
136
136
|
from panther.utils import load_env, timezone_now
|
137
137
|
|
138
138
|
BASE_DIR = Path(__name__).resolve().parent
|
@@ -144,7 +144,7 @@ MIDDLEWARES = [
|
|
144
144
|
{MONITORING}
|
145
145
|
]
|
146
146
|
|
147
|
-
|
147
|
+
InfoThrottle = Throttle(rate=5, duration=timedelta(minutes=1))
|
148
148
|
|
149
149
|
TIMEZONE = 'UTC'
|
150
150
|
|
@@ -154,7 +154,7 @@ async def hello_world_api():
|
|
154
154
|
return {'detail': 'Hello World'}
|
155
155
|
|
156
156
|
|
157
|
-
@API(cache=True, throttling=
|
157
|
+
@API(cache=True, throttling=InfoThrottle)
|
158
158
|
async def info_api(request: Request):
|
159
159
|
data = {
|
160
160
|
'panther_version': version(),
|
panther/cli/utils.py
CHANGED
@@ -23,43 +23,52 @@ else:
|
|
23
23
|
br = '╯'
|
24
24
|
bl = '╰'
|
25
25
|
|
26
|
-
top = f'{tl}{
|
27
|
-
bottom = f'{bl}{
|
26
|
+
top = f'{tl}{60 * v}{tr}'
|
27
|
+
bottom = f'{bl}{60 * v}{br}'
|
28
28
|
|
29
29
|
logo = rf"""{top}
|
30
|
-
{h}
|
31
|
-
{h}
|
32
|
-
{h}
|
33
|
-
{h}
|
34
|
-
{h}
|
35
|
-
{h}
|
36
|
-
{h}
|
37
|
-
{h}
|
38
|
-
|
39
|
-
help_message =
|
40
|
-
{h}
|
41
|
-
{h}
|
42
|
-
{h}
|
43
|
-
{h}
|
44
|
-
{h}
|
45
|
-
{h}
|
46
|
-
{h} *
|
47
|
-
{h}
|
48
|
-
{h}
|
49
|
-
{h}
|
50
|
-
{h}
|
51
|
-
{h}
|
52
|
-
{h}
|
53
|
-
{h}
|
54
|
-
{h}
|
55
|
-
{h}
|
56
|
-
{h}
|
57
|
-
{h}
|
58
|
-
{h}
|
59
|
-
{h}
|
60
|
-
{h}
|
61
|
-
{h}
|
62
|
-
{h}
|
30
|
+
{h} ____ __ __ {h}
|
31
|
+
{h} /\ _`\ /\ \__/\ \ {h}
|
32
|
+
{h} \ \ \L\ \ __ ___\ \ ,_\ \ \___ __ _ __ {h}
|
33
|
+
{h} \ \ ,__/'__`\ /' _ `\ \ \/\ \ _ `\ /'__`\/\`'__\ {h}
|
34
|
+
{h} \ \ \/\ \L\.\_/\ \/\ \ \ \_\ \ \ \ \/\ __/\ \ \/ {h}
|
35
|
+
{h} \ \_\ \__/.\_\ \_\ \_\ \__\\ \_\ \_\ \____\\ \_\ {h}
|
36
|
+
{h} \/_/\/__/\/_/\/_/\/_/\/__/ \/_/\/_/\/____/ \/_/ {h}
|
37
|
+
{h} {h}"""
|
38
|
+
|
39
|
+
help_message = rf"""{logo}
|
40
|
+
{h} Usage: panther <command> \[options] {h}
|
41
|
+
{h} {h}
|
42
|
+
{h} Commands: {h}
|
43
|
+
{h} - create \[project_name] \[directory] {h}
|
44
|
+
{h} Create a new Panther project. {h}
|
45
|
+
{h} * Interactive mode if no arguments provided. {h}
|
46
|
+
{h} * Non-interactive if project_name and directory {h}
|
47
|
+
{h} are specified (default directory: .). {h}
|
48
|
+
{h} Example: {h}
|
49
|
+
{h} - `panther create` {h}
|
50
|
+
{h} - `panther create myapp myapp` {h}
|
51
|
+
{h} {h}
|
52
|
+
{h} - run <app> \[options] {h}
|
53
|
+
{h} Run your Panther project using Uvicorn. {h}
|
54
|
+
{h} * app: address of your application. {h}
|
55
|
+
{h} * options: Check `uvicorn --help` for options. {h}
|
56
|
+
{h} * `panther run` is alias of `uvicorn`. {h}
|
57
|
+
{h} Example: `panther run main:app --reload` {h}
|
58
|
+
{h} {h}
|
59
|
+
{h} - shell <application_file> {h}
|
60
|
+
{h} Start an interactive Python shell with your app. {h}
|
61
|
+
{h} * application_file: path to your main app file. {h}
|
62
|
+
{h} Example: `panther shell main.py` {h}
|
63
|
+
{h} {h}
|
64
|
+
{h} - monitor {h}
|
65
|
+
{h} Display real-time request monitoring. {h}
|
66
|
+
{h} {h}
|
67
|
+
{h} - version | --version {h}
|
68
|
+
{h} Display the current version of Panther. {h}
|
69
|
+
{h} {h}
|
70
|
+
{h} - help | h | --help | -h {h}
|
71
|
+
{h} Show this help message and exit. {h}
|
63
72
|
{bottom}
|
64
73
|
"""
|
65
74
|
|
@@ -87,29 +96,10 @@ def cli_info(message: str) -> None:
|
|
87
96
|
logger.info('Use "panther -h" for more help\n')
|
88
97
|
|
89
98
|
|
90
|
-
def clean_args(args: list[str]) -> dict:
|
91
|
-
"""
|
92
|
-
Input: ['--reload', '--host', '127.0.0.1', ...]
|
93
|
-
Output: {'--reload: None, 'host': '127.0.0.1', ...}
|
94
|
-
"""
|
95
|
-
_args = {}
|
96
|
-
for i, arg in enumerate(args):
|
97
|
-
if arg.startswith('--'):
|
98
|
-
if (i + 1) < len(args):
|
99
|
-
_args[arg[2:]] = args[i + 1]
|
100
|
-
else:
|
101
|
-
_args[arg[2:]] = True
|
102
|
-
return _args
|
103
|
-
|
104
|
-
|
105
99
|
def print_help_message():
|
106
100
|
rprint(help_message)
|
107
101
|
|
108
102
|
|
109
|
-
def print_uvicorn_help_message():
|
110
|
-
rprint('Run `uvicorn --help` for more help')
|
111
|
-
|
112
|
-
|
113
103
|
def print_info(config: Config):
|
114
104
|
from panther.db.connections import redis
|
115
105
|
|
@@ -118,15 +108,12 @@ def print_info(config: Config):
|
|
118
108
|
bt = config.BACKGROUND_TASKS
|
119
109
|
ws = config.HAS_WS
|
120
110
|
rd = redis.is_connected
|
121
|
-
bd = '{
|
122
|
-
if len(bd) >
|
123
|
-
bd = f'{bd[:
|
111
|
+
bd = f'{config.BASE_DIR!s:<41}'
|
112
|
+
if len(bd) > 41:
|
113
|
+
bd = f'{bd[:38]}...'
|
124
114
|
|
125
115
|
# Monitoring
|
126
|
-
if config.MONITORING
|
127
|
-
monitor = f'{h} * Run "panther monitor" in another session for Monitoring{h}\n'
|
128
|
-
else:
|
129
|
-
monitor = None
|
116
|
+
monitor = f'{h} * Run "panther monitor" in another session for Monitoring {h}\n' if config.MONITORING else None
|
130
117
|
|
131
118
|
# Uvloop
|
132
119
|
uvloop_msg = None
|
@@ -135,25 +122,27 @@ def print_info(config: Config):
|
|
135
122
|
import uvloop
|
136
123
|
except ImportError:
|
137
124
|
uvloop_msg = (
|
138
|
-
f'{h} * You may want to install `uvloop` for better performance{h}\n'
|
139
|
-
f'{h} `pip install uvloop`
|
125
|
+
f'{h} * You may want to install `uvloop` for better performance {h}\n'
|
126
|
+
f'{h} `pip install uvloop` {h}\n'
|
127
|
+
)
|
140
128
|
|
141
129
|
# Gunicorn if Websocket
|
142
130
|
gunicorn_msg = None
|
143
131
|
if config.HAS_WS:
|
144
132
|
try:
|
145
133
|
import gunicorn
|
146
|
-
|
134
|
+
|
135
|
+
gunicorn_msg = f'{h} * You have WS, so make sure to run gunicorn with --preload {h}\n'
|
147
136
|
except ImportError:
|
148
137
|
pass
|
149
138
|
|
150
139
|
# Message
|
151
140
|
info_message = f"""{logo}
|
152
|
-
{h} Redis: {rd} \t
|
153
|
-
{h} Websocket: {ws} \t
|
154
|
-
{h} Monitoring: {mo} \t
|
155
|
-
{h} Log Queries: {lq} \t
|
156
|
-
{h} Background Tasks: {bt} \t
|
141
|
+
{h} Redis: {rd} \t {h}
|
142
|
+
{h} Websocket: {ws} \t {h}
|
143
|
+
{h} Monitoring: {mo} \t {h}
|
144
|
+
{h} Log Queries: {lq} \t {h}
|
145
|
+
{h} Background Tasks: {bt} \t {h}
|
157
146
|
{h} Base directory: {bd}{h}
|
158
147
|
"""
|
159
148
|
if monitor:
|