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.
Files changed (56) hide show
  1. panther/__init__.py +1 -1
  2. panther/_load_configs.py +46 -37
  3. panther/_utils.py +49 -34
  4. panther/app.py +96 -97
  5. panther/authentications.py +97 -50
  6. panther/background_tasks.py +98 -124
  7. panther/base_request.py +16 -10
  8. panther/base_websocket.py +8 -8
  9. panther/caching.py +16 -80
  10. panther/cli/create_command.py +17 -16
  11. panther/cli/main.py +1 -1
  12. panther/cli/monitor_command.py +11 -6
  13. panther/cli/run_command.py +5 -71
  14. panther/cli/template.py +7 -7
  15. panther/cli/utils.py +58 -69
  16. panther/configs.py +70 -72
  17. panther/db/connections.py +18 -24
  18. panther/db/cursor.py +0 -1
  19. panther/db/models.py +24 -8
  20. panther/db/queries/base_queries.py +2 -5
  21. panther/db/queries/mongodb_queries.py +17 -20
  22. panther/db/queries/pantherdb_queries.py +1 -1
  23. panther/db/queries/queries.py +26 -8
  24. panther/db/utils.py +1 -1
  25. panther/events.py +25 -14
  26. panther/exceptions.py +2 -7
  27. panther/file_handler.py +1 -1
  28. panther/generics.py +11 -8
  29. panther/logging.py +2 -1
  30. panther/main.py +12 -13
  31. panther/middlewares/cors.py +67 -0
  32. panther/middlewares/monitoring.py +5 -3
  33. panther/openapi/urls.py +2 -2
  34. panther/openapi/utils.py +3 -3
  35. panther/openapi/views.py +20 -37
  36. panther/pagination.py +4 -2
  37. panther/panel/apis.py +2 -7
  38. panther/panel/urls.py +2 -6
  39. panther/panel/utils.py +9 -5
  40. panther/panel/views.py +13 -22
  41. panther/permissions.py +2 -1
  42. panther/request.py +2 -1
  43. panther/response.py +53 -47
  44. panther/routings.py +12 -12
  45. panther/serializer.py +19 -20
  46. panther/test.py +73 -58
  47. panther/throttling.py +68 -3
  48. panther/utils.py +5 -11
  49. {panther-5.0.0b3.dist-info → panther-5.0.0b4.dist-info}/METADATA +1 -1
  50. panther-5.0.0b4.dist-info/RECORD +75 -0
  51. panther/monitoring.py +0 -34
  52. panther-5.0.0b3.dist-info/RECORD +0 -75
  53. {panther-5.0.0b3.dist-info → panther-5.0.0b4.dist-info}/WHEEL +0 -0
  54. {panther-5.0.0b3.dist-info → panther-5.0.0b4.dist-info}/entry_points.txt +0 -0
  55. {panther-5.0.0b3.dist-info → panther-5.0.0b4.dist-info}/licenses/LICENSE +0 -0
  56. {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 InvalidPathVariableAPIError, BaseError
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
- 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
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 types import NoneType
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, cache_exp_time: timedelta | None = None) -> str:
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 cache_exp_time:
27
- time = round_datetime(datetime.now(), cache_exp_time)
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 throttling_cache_key(request: Request, duration: timedelta) -> str:
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, cache_exp_time=cache_exp_time)
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, cache_exp_time: timedelta | int) -> None:
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, str, int] = (response.body.decode(), response.headers, response.status_code)
72
- cache_exp_time = cache_exp_time or config.DEFAULT_CACHE_EXP
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
- throttling_storage[key] += 1
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.')
@@ -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
- USER_MODEL_PART, REDIS_PART,
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
- self,
198
- message: str,
199
- default: str | bool,
200
- error_message: str,
201
- validation_func: Callable,
202
- show_validation_error: bool = False,
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.')
@@ -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 = date_time | method | path | ip:port | response_time(seconds) | status
55
+ # line = datetime | method | path | ip:port | response_time(seconds) | status
56
56
  columns = line.split('|')
57
- columns[4] = self._clean_response_time(columns[4])
58
- self.rows.append(columns)
59
- live.update(self.generate_table())
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 f'`{self.monitoring_log_file}` file not found. (Make sure `panther.middlewares.monitoring.MonitoringMiddleware` is in your `MIDDLEWARES`)'
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()
@@ -1,82 +1,16 @@
1
1
  import contextlib
2
- import os
2
+ import sys
3
3
 
4
4
  import uvicorn
5
5
 
6
- from panther.cli.utils import print_uvicorn_help_message, clean_args, cli_error
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
- uvicorn.run('main:app', **command)
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 InfoThrottling
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=InfoThrottling)
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 Throttling
42
+ from panther.throttling import Throttle
43
43
 
44
- InfoThrottling = Throttling(rate=5, duration=timedelta(minutes=1))
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 Throttling
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
- InfoThrottling = Throttling(rate=5, duration=timedelta(minutes=1))
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=InfoThrottling)
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}{58 * v}{tr}'
27
- bottom = f'{bl}{58 * v}{br}'
26
+ top = f'{tl}{60 * v}{tr}'
27
+ bottom = f'{bl}{60 * v}{br}'
28
28
 
29
29
  logo = rf"""{top}
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 = f"""{logo}
40
- {h} usage: {h}
41
- {h} - panther create {h}
42
- {h} Create project interactive {h}
43
- {h} {h}
44
- {h} - panther create <project_name> <directory> {h}
45
- {h} Default<directory> is `.` {h}
46
- {h} * It will create the project non-interactive {h}
47
- {h} {h}
48
- {h} - panther run [--reload | --help] {h}
49
- {h} Run your project with uvicorn {h}
50
- {h} {h}
51
- {h} - panther shell <application file path> {h}
52
- {h} Run interactive python shell {h}
53
- {h} * Example: `panther shell main.py` {h}
54
- {h} {h}
55
- {h} - panther monitor {h}
56
- {h} Show the monitor :) {h}
57
- {h} {h}
58
- {h} - panther version | --version {h}
59
- {h} Print the current version of Panther {h}
60
- {h} {h}
61
- {h} - panther h | help | --help | -h {h}
62
- {h} Show this message and exit {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 = '{0:<39}'.format(str(config.BASE_DIR))
122
- if len(bd) > 39:
123
- bd = f'{bd[:36]}...'
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` {h}\n')
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
- gunicorn_msg = f'{h} * You have WS, so make sure to run gunicorn with --preload{h}\n'
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 {h}
153
- {h} Websocket: {ws} \t {h}
154
- {h} Monitoring: {mo} \t {h}
155
- {h} Log Queries: {lq} \t {h}
156
- {h} Background Tasks: {bt} \t {h}
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: