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 CHANGED
@@ -1,6 +1,6 @@
1
1
  from panther.main import Panther # noqa: F401
2
2
 
3
- __version__ = '3.1.3'
3
+ __version__ = '3.1.5'
4
4
 
5
5
 
6
6
  def version():
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
- collected_models = []
117
-
118
- for root, _, files in os.walk(config['base_dir']):
119
- # Traverse through each directory
120
- for f in files:
121
- # Traverse through each file of directory
122
- if f == 'models.py':
123
- slash = '\\' if platform.system() == 'Windows' else '/'
124
-
125
- # If the file was "models.py" read it
126
- file_path = f'{root}{slash}models.py'
127
- with Path(file_path).open() as file:
128
- # Parse the file with ast
129
- node = ast.parse(file.read())
130
- for n in node.body:
131
- # Find classes in each element of files' body
132
- if type(n) is ast.ClassDef and n.bases:
133
- class_path = (
134
- file_path.removesuffix(f'{slash}models.py')
135
- .removeprefix(f'{config["base_dir"]}{slash}')
136
- .replace(slash, '.')
137
- )
138
- # We don't need to import the package classes
139
- if class_path.find('site-packages') == -1:
140
- # Import the class to check his parents and siblings
141
- klass = import_class(f'{class_path}.models.{n.name}')
142
-
143
- collected_models.extend(
144
- [
145
- {
146
- 'name': n.name,
147
- 'path': file_path,
148
- 'class': klass,
149
- 'app': class_path.split('.'),
150
- }
151
- for parent in klass.__mro__
152
- if parent is Model
153
- ]
154
- )
155
- return collected_models
156
-
157
-
158
- def load_urls(configs: dict, /, urls: dict | None) -> dict:
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
- from panther.logger import logger
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
- if monitoring is not None:
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,
@@ -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
@@ -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) -> Model:
87
+ def user(self):
90
88
  return self._user
91
89
 
92
- def set_user(self, user: Model) -> None:
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
 
@@ -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
- with Path(monitoring_log_file).open() as f:
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
- from panther.db.connection import redis
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
  }
@@ -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
- from panther.logger import query_logger
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
- query_logger.info(f'\033[1mQuery -->\033[0m {class_name}.{func.__name__}() --> {(end - start) * 1_000:.2} ms')
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.middlewares.monitoring import Middleware as MonitoringMiddleware
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
- """ We can't import logger on the top cause it needs config['base_dir'] ans its fill in __init__ """
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
- # Start Websocket Listener (Redis Required)
41
- Thread(
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
- if config['monitoring']:
50
- logger.info('Run "panther monitor" in another session for Monitoring.')
51
- if sys.version_info < (3, 11):
52
- logger.warning('Use Python Version 3.11+ For Better Performance.')
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 call in load_configs,
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
- async with asyncio.TaskGroup() as tg:
99
- tg.create_task(self.run(scope, receive, send))
113
+ await func(scope, receive, send)
100
114
  2.
101
- await self.run(scope, receive, send)
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(self.run, scope, receive, send)
105
- await anyio.to_thread.run_sync(self.run, scope, receive, send)
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(self.run, scope, receive, send)
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 Middleware
155
- monitoring_middleware = None
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=monitoring_middleware,
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=monitoring_middleware,
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=monitoring_middleware,
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=monitoring_middleware,
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=monitoring_middleware,
253
+ monitoring=self.monitoring,
238
254
  headers=response.headers,
239
255
  body=response.body,
240
256
  )
@@ -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
- return Redis(**self.kwargs)
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
- result = []
12
- for i, m in enumerate(config['models']):
13
- data = {
14
- 'name': m['name'],
15
- 'app': '.'.join(a for a in m['app']),
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
- from panther.logger import logger
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
- from panther.logger import logger
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
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 ~=5.0
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=B9pXYy0ONjwd2TV6rRd6ZBmRCRdZ_DkwoQw6zgG--kw,110
2
- panther/_load_configs.py,sha256=D3RPY5VOVzcmbp4AbzVJL8YHykiOo_h2Em3gvzHC64A,6996
3
- panther/_utils.py,sha256=A_cr2AZRtwiWFvPAEGufbmt3ktBlAUJ_rvbtxzEPtfs,4372
4
- panther/app.py,sha256=HnU2Mkj4991da-ln5DfmHrrPMXjQee4AJFcy6soDMyQ,7940
5
- panther/authentications.py,sha256=mjAjZ-AhL717qn3T6QtjFNTTLXTU_LPtHb0R9nHyGpo,4128
6
- panther/background_tasks.py,sha256=oHIEJsqPGN-qfjJTwhCJ_LO5mllQDmPOuwfZCUAlg5M,7204
7
- panther/base_request.py,sha256=N8oV-tRLNNo9n4shAD5tolT4u8yOV7yUz3A-5b9rSvw,2797
8
- panther/base_websocket.py,sha256=BpBtS6FO_swqeF5fx6-Sz6K2j5JVT-sL_toKsN7LJn4,6140
9
- panther/caching.py,sha256=vSUFb3SolXO3r1v-KSMoOt2T1Y1OnUyKb1Hj4xTyNcI,2532
10
- panther/configs.py,sha256=XSQ0AkT70r4lCa6s37DcfSpuh5xwOCHxO24DBynOTWE,1841
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/logger.py,sha256=aWUY9QtwjvrENpJuBmySx2b1MNn_zbwE5G8yFHMYSmM,2602
14
- panther/main.py,sha256=jPoG-ssIArUuCJJ6_AHzxm_Bmg_T6vgALS9uLpGS5TA,9894
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=LpQGbyLLLD9I9_7BAVRdk1hlbfekmsjDV10eJq6RjXQ,1752
17
+ panther/request.py,sha256=W3L_udvIxgjqVxxc8aaKDIQBVjdc7TTrFyoOJQ8M8FA,1773
17
18
  panther/response.py,sha256=HcL8nuXpZX_vhT9QUw0dnbtP75T_LbvKAWGT48_LPuU,3382
18
- panther/routings.py,sha256=3_4IFL7sXZBrCCnCfGVzogCAlqP_40w3b-sJBHLjgbU,5124
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=SeuAAG3UU6urOcWzmMQ7yG1Wt6f3o7ocNTtK53iDyLk,1622
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=ovnYCfySm4hlNGCTLSYKZAvRAYz8a9kF6JBTqpBhRCA,2191
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=a5nVysMM6gi5eh-GMNFIchl-1OL0YaBoAMmLuQ3aG0w,3549
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=X0XDonolAfZSvemn0MfuWWQYxwGUz-xZeLfsIm3jl6c,1325
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=qFnlda2CEP4BnOfwkhvjFIpVaI0JoLNXDntNulNgWpQ,7302
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/monitoring.py,sha256=7STDDmup3vEdzp3oHSO2HSqM1qYfy-gF-XgnqG8c9Xc,863
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=9sKeAbVeRJV-FSTgz88waeLcTFWzcP4iYm5tmBgPrJk,1834
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.3.dist-info/LICENSE,sha256=2aF1hL2aC0zRPjzUkSxJUzZbn2_uLoOkn7DHjzZni-I,1524
49
- panther-3.1.3.dist-info/METADATA,sha256=XYEK2AmEUPZ_0wnVvujumAYOA93VLZ7e1rTmElX8a4Q,6074
50
- panther-3.1.3.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
51
- panther-3.1.3.dist-info/entry_points.txt,sha256=6GPxYFGuzVfNB4YpHFJvYex6iWah5_tLnirAHwj2Qsg,51
52
- panther-3.1.3.dist-info/top_level.txt,sha256=VbBs02JGXTIoHMzsX-eLOk2MCbBZzQbLhWiYpI7xI2g,8
53
- panther-3.1.3.dist-info/RECORD,,
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}')