panther 3.8.2__py3-none-any.whl → 4.0.0__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 (52) hide show
  1. panther/__init__.py +1 -1
  2. panther/_load_configs.py +168 -171
  3. panther/_utils.py +26 -49
  4. panther/app.py +85 -105
  5. panther/authentications.py +86 -55
  6. panther/background_tasks.py +25 -14
  7. panther/base_request.py +38 -14
  8. panther/base_websocket.py +172 -94
  9. panther/caching.py +60 -25
  10. panther/cli/create_command.py +20 -10
  11. panther/cli/monitor_command.py +63 -37
  12. panther/cli/template.py +40 -20
  13. panther/cli/utils.py +32 -18
  14. panther/configs.py +65 -58
  15. panther/db/connections.py +139 -0
  16. panther/db/cursor.py +43 -0
  17. panther/db/models.py +64 -29
  18. panther/db/queries/__init__.py +1 -1
  19. panther/db/queries/base_queries.py +127 -0
  20. panther/db/queries/mongodb_queries.py +77 -38
  21. panther/db/queries/pantherdb_queries.py +59 -30
  22. panther/db/queries/queries.py +232 -117
  23. panther/db/utils.py +17 -18
  24. panther/events.py +44 -0
  25. panther/exceptions.py +26 -12
  26. panther/file_handler.py +2 -2
  27. panther/generics.py +163 -0
  28. panther/logging.py +7 -2
  29. panther/main.py +111 -188
  30. panther/middlewares/base.py +3 -0
  31. panther/monitoring.py +8 -5
  32. panther/pagination.py +48 -0
  33. panther/panel/apis.py +32 -5
  34. panther/panel/urls.py +2 -1
  35. panther/permissions.py +3 -3
  36. panther/request.py +6 -13
  37. panther/response.py +114 -34
  38. panther/routings.py +83 -66
  39. panther/serializer.py +214 -33
  40. panther/test.py +31 -21
  41. panther/utils.py +28 -16
  42. panther/websocket.py +7 -4
  43. {panther-3.8.2.dist-info → panther-4.0.0.dist-info}/METADATA +93 -71
  44. panther-4.0.0.dist-info/RECORD +57 -0
  45. {panther-3.8.2.dist-info → panther-4.0.0.dist-info}/WHEEL +1 -1
  46. panther/db/connection.py +0 -92
  47. panther/middlewares/db.py +0 -18
  48. panther/middlewares/redis.py +0 -47
  49. panther-3.8.2.dist-info/RECORD +0 -54
  50. {panther-3.8.2.dist-info → panther-4.0.0.dist-info}/LICENSE +0 -0
  51. {panther-3.8.2.dist-info → panther-4.0.0.dist-info}/entry_points.txt +0 -0
  52. {panther-3.8.2.dist-info → panther-4.0.0.dist-info}/top_level.txt +0 -0
@@ -16,8 +16,7 @@ from panther.cli.template import (
16
16
  AUTO_REFORMAT_PART,
17
17
  DATABASE_PANTHERDB_PART,
18
18
  DATABASE_MONGODB_PART,
19
- USER_MODEL_PART,
20
- PANTHERDB_ENCRYPTION,
19
+ USER_MODEL_PART, REDIS_PART,
21
20
  )
22
21
  from panther.cli.utils import cli_error
23
22
 
@@ -35,6 +34,7 @@ class CreateProject:
35
34
  self.base_directory = '.'
36
35
  self.database = '0'
37
36
  self.database_encryption = False
37
+ self.redis = False
38
38
  self.authentication = False
39
39
  self.monitoring = True
40
40
  self.log_queries = True
@@ -61,7 +61,7 @@ class CreateProject:
61
61
  },
62
62
  {
63
63
  'field': 'database',
64
- 'message': ' 0: PantherDB\n 1: MongoDB (Required `pymongo`)\n 2: No Database\nChoose Your Database (default is 0)',
64
+ 'message': ' 0: PantherDB (File-Base, No Requirements)\n 1: MongoDB (Required `pymongo`)\n 2: No Database\nChoose Your Database (default is 0)',
65
65
  'validation_func': lambda x: x in ['0', '1', '2'],
66
66
  'error_message': "Invalid Choice, '{}' not in ['0', '1', '2']",
67
67
  },
@@ -71,6 +71,11 @@ class CreateProject:
71
71
  'is_boolean': True,
72
72
  'condition': "self.database == '0'"
73
73
  },
74
+ {
75
+ 'field': 'redis',
76
+ 'message': 'Do You Want To Use Redis (Required `redis`)',
77
+ 'is_boolean': True,
78
+ },
74
79
  {
75
80
  'field': 'authentication',
76
81
  'message': 'Do You Want To Use JWT Authentication (Required `python-jose`)',
@@ -78,7 +83,7 @@ class CreateProject:
78
83
  },
79
84
  {
80
85
  'field': 'monitoring',
81
- 'message': 'Do You Want To Use Built-in Monitoring',
86
+ 'message': 'Do You Want To Use Built-in Monitoring (Required `watchfiles`)',
82
87
  'is_boolean': True,
83
88
  },
84
89
  {
@@ -88,7 +93,7 @@ class CreateProject:
88
93
  },
89
94
  {
90
95
  'field': 'auto_reformat',
91
- 'message': 'Do You Want To Use Auto Reformat (Required `ruff`)',
96
+ 'message': 'Do You Want To Use Auto Code Reformat (Required `ruff`)',
92
97
  'is_boolean': True,
93
98
  },
94
99
  ]
@@ -139,7 +144,9 @@ class CreateProject:
139
144
  monitoring_part = MONITORING_PART if self.monitoring else ''
140
145
  log_queries_part = LOG_QUERIES_PART if self.log_queries else ''
141
146
  auto_reformat_part = AUTO_REFORMAT_PART if self.auto_reformat else ''
142
- database_encryption = PANTHERDB_ENCRYPTION if self.database_encryption else ''
147
+ database_encryption = 'True' if self.database_encryption else 'False'
148
+ database_extension = 'pdb' if self.database_encryption else 'json'
149
+ redis_part = REDIS_PART if self.redis else ''
143
150
  if self.database == '0':
144
151
  database_part = DATABASE_PANTHERDB_PART
145
152
  elif self.database == '1':
@@ -153,7 +160,9 @@ class CreateProject:
153
160
  data = data.replace('{LOG_QUERIES}', log_queries_part)
154
161
  data = data.replace('{AUTO_REFORMAT}', auto_reformat_part)
155
162
  data = data.replace('{DATABASE}', database_part)
156
- data = data.replace('{PANTHERDB_ENCRYPTION}', database_encryption)
163
+ data = data.replace('{PANTHERDB_ENCRYPTION}', database_encryption) # Should be after `DATABASE`
164
+ data = data.replace('{PANTHERDB_EXTENSION}', database_extension) # Should be after `DATABASE`
165
+ data = data.replace('{REDIS}', redis_part)
157
166
 
158
167
  data = data.replace('{PROJECT_NAME}', self.project_name.lower())
159
168
  data = data.replace('{PANTHER_VERSION}', version())
@@ -168,19 +177,19 @@ class CreateProject:
168
177
  field_name = question.pop('field')
169
178
  question['default'] = getattr(self, field_name)
170
179
  is_boolean = question.pop('is_boolean', False)
171
- clean_output = str # Do Nothing
180
+ convert_output = str # Do Nothing
172
181
  if is_boolean:
173
182
  question['message'] += f' (default is {self._to_str(question["default"])})'
174
183
  question['validation_func'] = self._is_boolean
175
184
  question['error_message'] = "Invalid Choice, '{}' not in ['y', 'n']"
176
- clean_output = self._to_boolean
185
+ convert_output = self._to_boolean
177
186
 
178
187
  # Check Question Condition
179
188
  if 'condition' in question and eval(question.pop('condition')) is False:
180
189
  print(flush=True)
181
190
  # Ask Question
182
191
  else:
183
- setattr(self, field_name, clean_output(self.ask(**question)))
192
+ setattr(self, field_name, convert_output(self.ask(**question)))
184
193
  self.progress(i + 1)
185
194
 
186
195
  def ask(
@@ -193,6 +202,7 @@ class CreateProject:
193
202
  ) -> str:
194
203
  value = Prompt.ask(message, console=self.input_console).lower() or default
195
204
  while not validation_func(value):
205
+ # Remove the last line, show error message and ask again
196
206
  [print(end=self.REMOVE_LAST_LINE, flush=True) for _ in range(message.count('\n') + 1)]
197
207
  error = validation_func(value, return_error=True) if show_validation_error else value
198
208
  self.console.print(error_message.format(error), style='bold red')
@@ -1,71 +1,97 @@
1
1
  import contextlib
2
+ import logging
2
3
  import os
4
+ import signal
3
5
  from collections import deque
4
6
  from pathlib import Path
5
7
 
6
8
  from rich import box
7
9
  from rich.align import Align
8
10
  from rich.console import Group
9
- from rich.layout import Layout
10
11
  from rich.live import Live
11
12
  from rich.panel import Panel
12
13
  from rich.table import Table
13
- from watchfiles import watch
14
14
 
15
- from panther.cli.utils import cli_error
15
+ from panther.cli.utils import import_error
16
16
  from panther.configs import config
17
17
 
18
+ with contextlib.suppress(ImportError):
19
+ from watchfiles import watch
18
20
 
19
- def monitor() -> None:
20
- monitoring_log_file = Path(config['base_dir'] / 'logs' / 'monitoring.log')
21
+ loggerr = logging.getLogger('panther')
21
22
 
22
- def _generate_table(rows: deque) -> Panel:
23
- layout = Layout()
24
23
 
25
- rows = list(rows)
26
- _, lines = os.get_terminal_size()
24
+ class Monitoring:
25
+ def __init__(self):
26
+ self.rows = deque()
27
+ self.monitoring_log_file = Path(config.BASE_DIR / 'logs' / 'monitoring.log')
28
+
29
+ def monitor(self) -> None:
30
+ if error := self.initialize():
31
+ # Don't continue if initialize() has error
32
+ loggerr.error(error)
33
+ return
34
+
35
+ with (
36
+ self.monitoring_log_file.open() as f,
37
+ Live(
38
+ self.generate_table(),
39
+ vertical_overflow='visible',
40
+ screen=True,
41
+ ) as live,
42
+ contextlib.suppress(KeyboardInterrupt)
43
+ ):
44
+ f.readlines() # Set cursor at the end of the file
45
+
46
+ for _ in watch(self.monitoring_log_file):
47
+ for line in f.readlines():
48
+ self.rows.append(line.split('|'))
49
+ live.update(self.generate_table())
50
+
51
+ def initialize(self) -> str:
52
+ # Check requirements
53
+ try:
54
+ from watchfiles import watch
55
+ except ImportError as e:
56
+ return import_error(e, package='watchfiles').args[0]
57
+
58
+ # Check log file
59
+ if not self.monitoring_log_file.exists():
60
+ return f'`{self.monitoring_log_file}` file not found. (Make sure `MONITORING` is `True` in `configs`'
61
+
62
+ # Initialize Deque
63
+ self.update_rows()
64
+
65
+ # Register the signal handler
66
+ signal.signal(signal.SIGWINCH, self.update_rows)
67
+
68
+ def generate_table(self) -> Panel:
69
+ # 2023-03-24 01:42:52 | GET | /user/317/ | 127.0.0.1:48856 | 0.0366 ms | 200
27
70
 
28
71
  table = Table(box=box.MINIMAL_DOUBLE_HEAD)
29
72
  table.add_column('Datetime', justify='center', style='magenta', no_wrap=True)
30
- table.add_column('Method', justify='center', style='cyan')
31
- table.add_column('Path', justify='center', style='cyan')
73
+ table.add_column('Method', justify='center', style='cyan', no_wrap=True)
74
+ table.add_column('Path', justify='center', style='cyan', no_wrap=True)
32
75
  table.add_column('Client', justify='center', style='cyan')
33
76
  table.add_column('Response Time', justify='center', style='blue')
34
- table.add_column('Status Code', justify='center', style='blue')
77
+ table.add_column('Status', justify='center', style='blue', no_wrap=True)
35
78
 
36
- for row in rows[-lines:]: # It will give us "lines" last lines of "rows"
79
+ for row in self.rows:
37
80
  table.add_row(*row)
38
- layout.update(table)
39
81
 
40
82
  return Panel(
41
83
  Align.center(Group(table)),
42
84
  box=box.ROUNDED,
43
- padding=(1, 2),
85
+ padding=(0, 2),
44
86
  title='Monitoring',
45
87
  border_style='bright_blue',
46
88
  )
47
89
 
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)')
90
+ def update_rows(self, *args, **kwargs):
91
+ # Top = -4, Bottom = -2 --> -6
92
+ # Print of each line needs two line, so --> x // 2
93
+ lines = (os.get_terminal_size()[1] - 6) // 2
94
+ self.rows = deque(self.rows, maxlen=lines)
50
95
 
51
- with monitoring_log_file.open() as f:
52
- f.readlines() # Set cursor at the end of file
53
96
 
54
- _, init_lines_count = os.get_terminal_size()
55
- messages = deque(maxlen=init_lines_count - 10) # Save space for header and footer
56
-
57
- with (
58
- Live(
59
- _generate_table(messages),
60
- auto_refresh=False,
61
- vertical_overflow='visible',
62
- screen=True,
63
- ) as live,
64
- contextlib.suppress(KeyboardInterrupt),
65
- ):
66
- for _ in watch(monitoring_log_file):
67
- data = f.readline().split('|')
68
- # 2023-03-24 01:42:52 | GET | /user/317/ | 127.0.0.1:48856 | 0.0366 ms | 200
69
- messages.append(data)
70
- live.update(_generate_table(messages))
71
- live.refresh()
97
+ monitor = Monitoring().monitor
panther/cli/template.py CHANGED
@@ -11,6 +11,7 @@ from panther import status, version
11
11
  from panther.app import API
12
12
  from panther.request import Request
13
13
  from panther.response import Response
14
+ from panther.utils import timezone_now
14
15
 
15
16
 
16
17
  @API()
@@ -24,7 +25,7 @@ async def info_api(request: Request):
24
25
  'panther_version': version(),
25
26
  'method': request.method,
26
27
  'query_params': request.query_params,
27
- 'datetime_now': datetime.now().isoformat(),
28
+ 'datetime_now': timezone_now().isoformat(),
28
29
  'user_agent': request.headers.user_agent,
29
30
  }
30
31
  return Response(data=data, status_code=status.HTTP_202_ACCEPTED)
@@ -55,19 +56,19 @@ configs_py = """\"""
55
56
  {PROJECT_NAME} Project (Generated by Panther on %s)
56
57
  \"""
57
58
 
58
- from datetime import timedelta
59
59
  from pathlib import Path
60
60
 
61
- from panther.throttling import Throttling
62
61
  from panther.utils import load_env
63
62
 
64
63
  BASE_DIR = Path(__name__).resolve().parent
65
64
  env = load_env(BASE_DIR / '.env')
66
65
 
67
- SECRET_KEY = env['SECRET_KEY']{DATABASE}{USER_MODEL}{AUTHENTICATION}{MONITORING}{LOG_QUERIES}{AUTO_REFORMAT}{PANTHERDB_ENCRYPTION}
66
+ SECRET_KEY = env['SECRET_KEY']{DATABASE}{REDIS}{USER_MODEL}{AUTHENTICATION}{MONITORING}{LOG_QUERIES}{AUTO_REFORMAT}
68
67
 
69
68
  # More Info: https://PantherPy.GitHub.io/urls/
70
69
  URLs = 'core.urls.url_routing'
70
+
71
+ TIMEZONE = 'UTC'
71
72
  """ % datetime.now().date().isoformat()
72
73
 
73
74
  env = """SECRET_KEY='%s'
@@ -99,6 +100,7 @@ requirements = """panther==%s
99
100
 
100
101
  TEMPLATE = {
101
102
  'app': {
103
+ '__init__.py': '',
102
104
  'apis.py': apis_py,
103
105
  'models.py': models_py,
104
106
  'serializers.py': serializers_py,
@@ -106,6 +108,7 @@ TEMPLATE = {
106
108
  'urls.py': app_urls_py,
107
109
  },
108
110
  'core': {
111
+ '__init__.py': '',
109
112
  'configs.py': configs_py,
110
113
  'urls.py': urls_py,
111
114
  },
@@ -126,15 +129,16 @@ from panther.app import API
126
129
  from panther.request import Request
127
130
  from panther.response import Response
128
131
  from panther.throttling import Throttling
129
- from panther.utils import load_env
132
+ from panther.utils import load_env, timezone_now
130
133
 
131
134
  BASE_DIR = Path(__name__).resolve().parent
132
135
  env = load_env(BASE_DIR / '.env')
133
136
 
134
- SECRET_KEY = env['SECRET_KEY']{DATABASE}{USER_MODEL}{AUTHENTICATION}{MONITORING}{LOG_QUERIES}{AUTO_REFORMAT}{PANTHERDB_ENCRYPTION}
137
+ SECRET_KEY = env['SECRET_KEY']{DATABASE}{REDIS}{USER_MODEL}{AUTHENTICATION}{MONITORING}{LOG_QUERIES}{AUTO_REFORMAT}
135
138
 
136
139
  InfoThrottling = Throttling(rate=5, duration=timedelta(minutes=1))
137
140
 
141
+ TIMEZONE = 'UTC'
138
142
 
139
143
  @API()
140
144
  async def hello_world_api():
@@ -147,7 +151,7 @@ async def info_api(request: Request):
147
151
  'panther_version': version(),
148
152
  'method': request.method,
149
153
  'query_params': request.query_params,
150
- 'datetime_now': datetime.now().isoformat(),
154
+ 'datetime_now': timezone_now().isoformat(),
151
155
  'user_agent': request.headers.user_agent,
152
156
  }
153
157
  return Response(data=data, status_code=status.HTTP_202_ACCEPTED)
@@ -155,10 +159,11 @@ async def info_api(request: Request):
155
159
 
156
160
  url_routing = {
157
161
  '/': hello_world_api,
162
+ 'info/': info_api,
158
163
  }
159
164
 
160
165
  app = Panther(__name__, configs=__name__, urls=url_routing)
161
- """
166
+ """ % datetime.now().date().isoformat()
162
167
 
163
168
  SINGLE_FILE_TEMPLATE = {
164
169
  'main.py': single_main_py,
@@ -168,18 +173,37 @@ SINGLE_FILE_TEMPLATE = {
168
173
  }
169
174
 
170
175
  DATABASE_PANTHERDB_PART = """
171
- # More Info: Https://PantherPy.GitHub.io/middlewares/
172
176
 
173
- MIDDLEWARES = [
174
- ('panther.middlewares.db.DatabaseMiddleware', {'url': f'pantherdb://{BASE_DIR}/database.pdb'}),
175
- ]"""
177
+ # More Info: https://PantherPy.GitHub.io/database/
178
+ DATABASE = {
179
+ 'engine': {
180
+ 'class': 'panther.db.connections.PantherDBConnection',
181
+ 'path': BASE_DIR / 'database.{PANTHERDB_EXTENSION}',
182
+ 'encryption': {PANTHERDB_ENCRYPTION}
183
+ }
184
+ }"""
176
185
 
177
186
  DATABASE_MONGODB_PART = """
178
- # More Info: Https://PantherPy.GitHub.io/middlewares/
179
187
 
180
- MIDDLEWARES = [
181
- ('panther.middlewares.db.DatabaseMiddleware', {'url': f'mongodb://127.0.0.1:27017/{PROJECT_NAME}'}),
182
- ]"""
188
+ # More Info: https://PantherPy.GitHub.io/database/
189
+ DATABASE = {
190
+ 'engine': {
191
+ 'class': 'panther.db.connections.MongoDBConnection',
192
+ 'host': '127.0.0.1',
193
+ 'port': 27017,
194
+ 'database': '{PROJECT_NAME}'
195
+ }
196
+ }"""
197
+
198
+ REDIS_PART = """
199
+
200
+ # More Info: https://PantherPy.GitHub.io/redis/
201
+ REDIS = {
202
+ 'class': 'panther.db.connections.RedisConnection',
203
+ 'host': '127.0.0.1',
204
+ 'port': 6379,
205
+ 'db': 0,
206
+ }"""
183
207
 
184
208
  USER_MODEL_PART = """
185
209
 
@@ -205,7 +229,3 @@ AUTO_REFORMAT_PART = """
205
229
 
206
230
  # More Info: https://pantherpy.github.io/configs/#auto_reformat/
207
231
  AUTO_REFORMAT = True"""
208
-
209
- PANTHERDB_ENCRYPTION = """
210
-
211
- PANTHERDB_ENCRYPTION = True"""
panther/cli/utils.py CHANGED
@@ -3,11 +3,11 @@ import platform
3
3
 
4
4
  from rich import print as rprint
5
5
 
6
- from panther.exceptions import PantherException
6
+ from panther.configs import Config
7
+ from panther.exceptions import PantherError
7
8
 
8
9
  logger = logging.getLogger('panther')
9
10
 
10
-
11
11
  if platform.system() == 'Windows':
12
12
  h = '|'
13
13
  v = '_'
@@ -63,11 +63,11 @@ help_message = f"""{logo}
63
63
  """
64
64
 
65
65
 
66
- def import_error(message: str | Exception, package: str | None = None) -> None:
66
+ def import_error(message: str | Exception, package: str | None = None) -> PantherError:
67
67
  msg = str(message)
68
68
  if package:
69
69
  msg += f' -> Hint: `pip install {package}`'
70
- raise PantherException(msg)
70
+ return PantherError(msg)
71
71
 
72
72
 
73
73
  def cli_error(message: str | Exception) -> None:
@@ -109,44 +109,58 @@ def print_uvicorn_help_message():
109
109
  rprint('Run `uvicorn --help` for more help')
110
110
 
111
111
 
112
- def print_info(config: dict):
113
- mo = config['monitoring']
114
- lq = config['log_queries']
115
- bt = config['background_tasks']
116
- ws = config['has_ws']
117
- bd = '{0:<39}'.format(str(config['base_dir']))
112
+ def print_info(config: Config):
113
+ from panther.db.connections import redis
114
+
115
+ mo = config.MONITORING
116
+ lq = config.LOG_QUERIES
117
+ bt = config.BACKGROUND_TASKS
118
+ ws = config.HAS_WS
119
+ rd = redis.is_connected
120
+ bd = '{0:<39}'.format(str(config.BASE_DIR))
118
121
  if len(bd) > 39:
119
122
  bd = f'{bd[:36]}...'
120
123
 
121
124
  # Monitoring
122
- if config['monitoring']:
125
+ if config.MONITORING:
123
126
  monitor = f'{h} * Run "panther monitor" in another session for Monitoring{h}\n'
124
127
  else:
125
128
  monitor = None
126
129
 
127
130
  # Uvloop
131
+ uvloop_msg = None
128
132
  if platform.system() != 'Windows':
129
133
  try:
130
134
  import uvloop
131
- uvloop = None
132
135
  except ImportError:
133
- uvloop = (
136
+ uvloop_msg = (
134
137
  f'{h} * You may want to install `uvloop` for better performance{h}\n'
135
138
  f'{h} `pip install uvloop` {h}\n')
136
- else:
137
- uvloop = None
139
+
140
+ # Gunicorn if Websocket
141
+ gunicorn_msg = None
142
+ if config.HAS_WS:
143
+ try:
144
+ import gunicorn
145
+ gunicorn_msg = f'{h} * You have WS so make sure to run gunicorn with --preload{h}\n'
146
+ except ImportError:
147
+ pass
138
148
 
139
149
  # Message
140
150
  info_message = f"""{logo}
151
+ {h} Redis: {rd} \t {h}
152
+ {h} Websocket: {ws} \t {h}
141
153
  {h} Monitoring: {mo} \t {h}
142
154
  {h} Log Queries: {lq} \t {h}
143
155
  {h} Background Tasks: {bt} \t {h}
144
- {h} Websocket: {ws} \t {h}
145
156
  {h} Base directory: {bd}{h}
146
157
  """
147
158
  if monitor:
148
159
  info_message += monitor
149
- if uvloop:
150
- info_message += uvloop
160
+ if uvloop_msg:
161
+ info_message += uvloop_msg
162
+ if gunicorn_msg:
163
+ info_message += gunicorn_msg
164
+
151
165
  info_message += bottom
152
166
  rprint(info_message)
panther/configs.py CHANGED
@@ -1,3 +1,4 @@
1
+ import copy
1
2
  import typing
2
3
  from dataclasses import dataclass
3
4
  from datetime import timedelta
@@ -7,7 +8,6 @@ from typing import Callable
7
8
  from pydantic._internal._model_construction import ModelMetaclass
8
9
 
9
10
  from panther.throttling import Throttling
10
- from panther.utils import Singleton
11
11
 
12
12
 
13
13
  class JWTConfig:
@@ -41,71 +41,78 @@ class QueryObservable:
41
41
  @classmethod
42
42
  def update(cls):
43
43
  for observer in cls.observers:
44
- observer._reload_bases(parent=config.query_engine)
44
+ observer._reload_bases(parent=config.QUERY_ENGINE)
45
45
 
46
46
 
47
47
  @dataclass
48
- class Config(Singleton):
49
- base_dir: Path
50
- monitoring: bool
51
- log_queries: bool
52
- default_cache_exp: timedelta | None
53
- throttling: Throttling | None
54
- secret_key: bytes | None
55
- http_middlewares: list
56
- ws_middlewares: list
57
- reversed_http_middlewares: list
58
- reversed_ws_middlewares: list
59
- user_model: ModelMetaclass | None
60
- authentication: ModelMetaclass | None
61
- jwt_config: JWTConfig | None
62
- models: list[dict]
63
- flat_urls: dict
64
- urls: dict
65
- query_engine: typing.Callable | None
66
- websocket_connections: typing.Callable | None
67
- background_tasks: bool
68
- has_ws: bool
69
- startup: Callable | None
70
- shutdown: Callable | None
71
- auto_reformat: bool
72
- pantherdb_encryption: bool
48
+ class Config:
49
+ BASE_DIR: Path
50
+ MONITORING: bool
51
+ LOG_QUERIES: bool
52
+ DEFAULT_CACHE_EXP: timedelta | None
53
+ THROTTLING: Throttling | None
54
+ SECRET_KEY: bytes | None
55
+ HTTP_MIDDLEWARES: list[tuple]
56
+ WS_MIDDLEWARES: list[tuple]
57
+ USER_MODEL: ModelMetaclass | None
58
+ AUTHENTICATION: ModelMetaclass | None
59
+ WS_AUTHENTICATION: ModelMetaclass | None
60
+ JWT_CONFIG: JWTConfig | None
61
+ MODELS: list[dict]
62
+ FLAT_URLS: dict
63
+ URLS: dict
64
+ WEBSOCKET_CONNECTIONS: typing.Callable | None
65
+ BACKGROUND_TASKS: bool
66
+ HAS_WS: bool
67
+ STARTUPS: list[Callable]
68
+ SHUTDOWNS: list[Callable]
69
+ TIMEZONE: str
70
+ AUTO_REFORMAT: bool
71
+ QUERY_ENGINE: typing.Callable | None
72
+ DATABASE: typing.Callable | None
73
73
 
74
74
  def __setattr__(self, key, value):
75
75
  super().__setattr__(key, value)
76
- if key == 'query_engine':
76
+ if key == 'QUERY_ENGINE' and value:
77
77
  QueryObservable.update()
78
78
 
79
79
  def __setitem__(self, key, value):
80
- setattr(self, key, value)
80
+ setattr(self, key.upper(), value)
81
81
 
82
82
  def __getitem__(self, item):
83
- return getattr(self, item)
84
-
85
-
86
- config = Config(
87
- base_dir=Path(),
88
- monitoring=False,
89
- log_queries=False,
90
- default_cache_exp=None,
91
- throttling=None,
92
- secret_key=None,
93
- http_middlewares=[],
94
- ws_middlewares=[],
95
- reversed_http_middlewares=[],
96
- reversed_ws_middlewares=[],
97
- user_model=None,
98
- authentication=None,
99
- jwt_config=None,
100
- models=[],
101
- flat_urls={},
102
- urls={},
103
- query_engine=None,
104
- websocket_connections=None,
105
- background_tasks=False,
106
- has_ws=False,
107
- startup=None,
108
- shutdown=None,
109
- auto_reformat=False,
110
- pantherdb_encryption=False,
111
- )
83
+ return getattr(self, item.upper())
84
+
85
+ def refresh(self):
86
+ # In some tests we need to `refresh` the `config` values
87
+ for key, value in copy.deepcopy(default_configs).items():
88
+ setattr(self, key, value)
89
+
90
+
91
+ default_configs = {
92
+ 'BASE_DIR': Path(),
93
+ 'MONITORING': False,
94
+ 'LOG_QUERIES': False,
95
+ 'DEFAULT_CACHE_EXP': None,
96
+ 'THROTTLING': None,
97
+ 'SECRET_KEY': None,
98
+ 'HTTP_MIDDLEWARES': [],
99
+ 'WS_MIDDLEWARES': [],
100
+ 'USER_MODEL': None,
101
+ 'AUTHENTICATION': None,
102
+ 'WS_AUTHENTICATION': None,
103
+ 'JWT_CONFIG': None,
104
+ 'MODELS': [],
105
+ 'FLAT_URLS': {},
106
+ 'URLS': {},
107
+ 'WEBSOCKET_CONNECTIONS': None,
108
+ 'BACKGROUND_TASKS': False,
109
+ 'HAS_WS': False,
110
+ 'STARTUPS': [],
111
+ 'SHUTDOWNS': [],
112
+ 'TIMEZONE': 'UTC',
113
+ 'AUTO_REFORMAT': False,
114
+ 'QUERY_ENGINE': None,
115
+ 'DATABASE': None,
116
+ }
117
+
118
+ config = Config(**copy.deepcopy(default_configs))