squirrels 0.5.0rc0__py3-none-any.whl → 0.5.1__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.

Potentially problematic release.


This version of squirrels might be problematic. Click here for more details.

Files changed (108) hide show
  1. dateutils/__init__.py +6 -0
  2. dateutils/_enums.py +25 -0
  3. squirrels/dateutils.py → dateutils/_implementation.py +58 -111
  4. dateutils/types.py +6 -0
  5. squirrels/__init__.py +10 -12
  6. squirrels/_api_routes/__init__.py +5 -0
  7. squirrels/_api_routes/auth.py +271 -0
  8. squirrels/_api_routes/base.py +171 -0
  9. squirrels/_api_routes/dashboards.py +158 -0
  10. squirrels/_api_routes/data_management.py +148 -0
  11. squirrels/_api_routes/datasets.py +265 -0
  12. squirrels/_api_routes/oauth2.py +298 -0
  13. squirrels/_api_routes/project.py +252 -0
  14. squirrels/_api_server.py +245 -781
  15. squirrels/_arguments/__init__.py +0 -0
  16. squirrels/{arguments → _arguments}/init_time_args.py +7 -2
  17. squirrels/{arguments → _arguments}/run_time_args.py +13 -35
  18. squirrels/_auth.py +720 -212
  19. squirrels/_command_line.py +81 -41
  20. squirrels/_compile_prompts.py +147 -0
  21. squirrels/_connection_set.py +16 -7
  22. squirrels/_constants.py +29 -9
  23. squirrels/{_dashboards_io.py → _dashboards.py} +87 -6
  24. squirrels/_data_sources.py +570 -0
  25. squirrels/{dataset_result.py → _dataset_types.py} +2 -4
  26. squirrels/_exceptions.py +9 -37
  27. squirrels/_initializer.py +83 -59
  28. squirrels/_logging.py +117 -0
  29. squirrels/_manifest.py +129 -62
  30. squirrels/_model_builder.py +10 -52
  31. squirrels/_model_configs.py +3 -3
  32. squirrels/_model_queries.py +1 -1
  33. squirrels/_models.py +249 -118
  34. squirrels/{package_data → _package_data}/base_project/.env +16 -4
  35. squirrels/{package_data → _package_data}/base_project/.env.example +15 -3
  36. squirrels/{package_data → _package_data}/base_project/connections.yml +4 -3
  37. squirrels/{package_data → _package_data}/base_project/dashboards/dashboard_example.py +4 -4
  38. squirrels/_package_data/base_project/dashboards/dashboard_example.yml +22 -0
  39. squirrels/{package_data → _package_data}/base_project/duckdb_init.sql +1 -0
  40. squirrels/_package_data/base_project/macros/macros_example.sql +17 -0
  41. squirrels/{package_data → _package_data}/base_project/models/builds/build_example.py +2 -2
  42. squirrels/{package_data → _package_data}/base_project/models/builds/build_example.sql +1 -1
  43. squirrels/{package_data → _package_data}/base_project/models/builds/build_example.yml +2 -0
  44. squirrels/_package_data/base_project/models/dbviews/dbview_example.sql +17 -0
  45. squirrels/_package_data/base_project/models/dbviews/dbview_example.yml +32 -0
  46. squirrels/_package_data/base_project/models/federates/federate_example.py +48 -0
  47. squirrels/_package_data/base_project/models/federates/federate_example.sql +21 -0
  48. squirrels/{package_data → _package_data}/base_project/models/federates/federate_example.yml +7 -7
  49. squirrels/{package_data → _package_data}/base_project/models/sources.yml +5 -6
  50. squirrels/{package_data → _package_data}/base_project/parameters.yml +32 -45
  51. squirrels/_package_data/base_project/pyconfigs/connections.py +18 -0
  52. squirrels/{package_data → _package_data}/base_project/pyconfigs/context.py +31 -22
  53. squirrels/_package_data/base_project/pyconfigs/parameters.py +141 -0
  54. squirrels/_package_data/base_project/pyconfigs/user.py +44 -0
  55. squirrels/{package_data → _package_data}/base_project/seeds/seed_categories.yml +1 -1
  56. squirrels/{package_data → _package_data}/base_project/seeds/seed_subcategories.yml +1 -1
  57. squirrels/_package_data/base_project/squirrels.yml.j2 +61 -0
  58. squirrels/_package_data/templates/dataset_results.html +112 -0
  59. squirrels/_package_data/templates/oauth_login.html +271 -0
  60. squirrels/_package_data/templates/squirrels_studio.html +20 -0
  61. squirrels/_parameter_configs.py +76 -55
  62. squirrels/_parameter_options.py +348 -0
  63. squirrels/_parameter_sets.py +53 -45
  64. squirrels/_parameters.py +1664 -0
  65. squirrels/_project.py +403 -242
  66. squirrels/_py_module.py +3 -2
  67. squirrels/_request_context.py +33 -0
  68. squirrels/_schemas/__init__.py +0 -0
  69. squirrels/_schemas/auth_models.py +167 -0
  70. squirrels/_schemas/query_param_models.py +75 -0
  71. squirrels/{_api_response_models.py → _schemas/response_models.py} +48 -18
  72. squirrels/_seeds.py +1 -1
  73. squirrels/_sources.py +23 -19
  74. squirrels/_utils.py +121 -39
  75. squirrels/_version.py +1 -1
  76. squirrels/arguments.py +7 -0
  77. squirrels/auth.py +4 -0
  78. squirrels/connections.py +3 -0
  79. squirrels/dashboards.py +2 -81
  80. squirrels/data_sources.py +14 -563
  81. squirrels/parameter_options.py +13 -348
  82. squirrels/parameters.py +14 -1266
  83. squirrels/types.py +16 -0
  84. {squirrels-0.5.0rc0.dist-info → squirrels-0.5.1.dist-info}/METADATA +42 -30
  85. squirrels-0.5.1.dist-info/RECORD +98 -0
  86. squirrels/package_data/base_project/dashboards/dashboard_example.yml +0 -22
  87. squirrels/package_data/base_project/macros/macros_example.sql +0 -15
  88. squirrels/package_data/base_project/models/dbviews/dbview_example.sql +0 -12
  89. squirrels/package_data/base_project/models/dbviews/dbview_example.yml +0 -26
  90. squirrels/package_data/base_project/models/federates/federate_example.py +0 -44
  91. squirrels/package_data/base_project/models/federates/federate_example.sql +0 -17
  92. squirrels/package_data/base_project/pyconfigs/connections.py +0 -14
  93. squirrels/package_data/base_project/pyconfigs/parameters.py +0 -93
  94. squirrels/package_data/base_project/pyconfigs/user.py +0 -23
  95. squirrels/package_data/base_project/squirrels.yml.j2 +0 -71
  96. squirrels-0.5.0rc0.dist-info/RECORD +0 -70
  97. /squirrels/{package_data → _package_data}/base_project/assets/expenses.db +0 -0
  98. /squirrels/{package_data → _package_data}/base_project/assets/weather.db +0 -0
  99. /squirrels/{package_data → _package_data}/base_project/docker/.dockerignore +0 -0
  100. /squirrels/{package_data → _package_data}/base_project/docker/Dockerfile +0 -0
  101. /squirrels/{package_data → _package_data}/base_project/docker/compose.yml +0 -0
  102. /squirrels/{package_data/base_project/.gitignore → _package_data/base_project/gitignore} +0 -0
  103. /squirrels/{package_data → _package_data}/base_project/seeds/seed_categories.csv +0 -0
  104. /squirrels/{package_data → _package_data}/base_project/seeds/seed_subcategories.csv +0 -0
  105. /squirrels/{package_data → _package_data}/base_project/tmp/.gitignore +0 -0
  106. {squirrels-0.5.0rc0.dist-info → squirrels-0.5.1.dist-info}/WHEEL +0 -0
  107. {squirrels-0.5.0rc0.dist-info → squirrels-0.5.1.dist-info}/entry_points.txt +0 -0
  108. {squirrels-0.5.0rc0.dist-info → squirrels-0.5.1.dist-info}/licenses/LICENSE +0 -0
squirrels/_initializer.py CHANGED
@@ -1,35 +1,37 @@
1
1
  from typing import Optional
2
2
  from datetime import datetime
3
+ from pathlib import Path
3
4
  import inquirer, os, shutil, secrets
4
5
 
5
6
  from . import _constants as c, _utils as u
6
7
 
7
- base_proj_dir = u.Path(os.path.dirname(__file__), c.PACKAGE_DATA_FOLDER, c.BASE_PROJECT_FOLDER)
8
+ base_proj_dir = Path(os.path.dirname(__file__), c.PACKAGE_DATA_FOLDER, c.BASE_PROJECT_FOLDER)
8
9
 
9
10
  TMP_FOLDER = "tmp"
10
11
 
11
12
 
12
13
  class Initializer:
13
- def __init__(self, *, project_name: Optional[str] = None, overwrite: bool = False):
14
- self.project_name = project_name
15
- self.overwrite = overwrite
14
+ def __init__(self, *, project_name: Optional[str] = None, use_curr_dir: bool = False):
15
+ self.project_name = project_name if not use_curr_dir else None
16
+ self.use_curr_dir = use_curr_dir
16
17
 
17
- def _path_exists(self, filepath: u.Path) -> bool:
18
+ def _path_exists(self, filepath: Path) -> bool:
18
19
  return os.path.exists(filepath)
19
20
 
20
- def _files_have_same_content(self, file1: u.Path, file2: u.Path) -> bool:
21
+ def _files_have_same_content(self, file1: Path, file2: Path) -> bool:
21
22
  with open(file1, 'rb') as f1, open(file2, 'rb') as f2:
22
23
  return f1.read() == f2.read()
23
24
 
24
- def _add_timestamp_to_filename(self, path: u.Path) -> u.Path:
25
+ def _add_timestamp_to_filename(self, path: Path) -> Path:
25
26
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
26
27
  new_filename = f"{path.stem}_{timestamp}{path.suffix}"
27
28
  return path.with_name(new_filename)
28
29
 
29
- def _copy_file(self, filepath: u.Path, *, src_folder: str = ""):
30
- src_path = u.Path(base_proj_dir, src_folder, filepath)
30
+ def _copy_file(self, filepath: Path, *, src_folder: str = "", src_file: Path | None = None):
31
+ src_file = src_file if src_file is not None else filepath
32
+ src_path = Path(base_proj_dir, src_folder, src_file)
31
33
 
32
- filepath2 = u.Path(self.project_name, filepath) if self.project_name else filepath
34
+ filepath2 = Path(self.project_name, filepath) if self.project_name else filepath
33
35
  dest_dir = os.path.dirname(filepath2)
34
36
  if dest_dir != "":
35
37
  os.makedirs(dest_dir, exist_ok=True)
@@ -40,8 +42,6 @@ class Initializer:
40
42
  if self._files_have_same_content(src_path, filepath2):
41
43
  perform_copy = False
42
44
  extra_msg = "Skipping... file contents is same as source"
43
- elif self.overwrite:
44
- extra_msg = "Overwriting file..."
45
45
  else:
46
46
  filepath2 = self._add_timestamp_to_filename(old_filepath)
47
47
  extra_msg = f'Creating file as "{filepath2}" instead...'
@@ -53,38 +53,38 @@ class Initializer:
53
53
  shutil.copy(src_path, filepath2)
54
54
 
55
55
  def _copy_macros_file(self, filepath: str):
56
- self._copy_file(u.Path(c.MACROS_FOLDER, filepath))
56
+ self._copy_file(Path(c.MACROS_FOLDER, filepath))
57
57
 
58
58
  def _copy_models_file(self, filepath: str):
59
- self._copy_file(u.Path(c.MODELS_FOLDER, filepath))
59
+ self._copy_file(Path(c.MODELS_FOLDER, filepath))
60
60
 
61
61
  def _copy_build_file(self, filepath: str):
62
- self._copy_file(u.Path(c.MODELS_FOLDER, c.BUILDS_FOLDER, filepath))
62
+ self._copy_file(Path(c.MODELS_FOLDER, c.BUILDS_FOLDER, filepath))
63
63
 
64
64
  def _copy_dbview_file(self, filepath: str):
65
- self._copy_file(u.Path(c.MODELS_FOLDER, c.DBVIEWS_FOLDER, filepath))
65
+ self._copy_file(Path(c.MODELS_FOLDER, c.DBVIEWS_FOLDER, filepath))
66
66
 
67
67
  def _copy_federate_file(self, filepath: str):
68
- self._copy_file(u.Path(c.MODELS_FOLDER, c.FEDERATES_FOLDER, filepath))
68
+ self._copy_file(Path(c.MODELS_FOLDER, c.FEDERATES_FOLDER, filepath))
69
69
 
70
70
  def _copy_database_file(self, filepath: str):
71
- self._copy_file(u.Path(c.DATABASE_FOLDER, filepath))
71
+ self._copy_file(Path(c.DATABASE_FOLDER, filepath))
72
72
 
73
73
  def _copy_pyconfig_file(self, filepath: str):
74
- self._copy_file(u.Path(c.PYCONFIGS_FOLDER, filepath))
74
+ self._copy_file(Path(c.PYCONFIGS_FOLDER, filepath))
75
75
 
76
76
  def _copy_seed_file(self, filepath: str):
77
- self._copy_file(u.Path(c.SEEDS_FOLDER, filepath))
77
+ self._copy_file(Path(c.SEEDS_FOLDER, filepath))
78
78
 
79
79
  def _copy_dashboard_file(self, filepath: str):
80
- self._copy_file(u.Path(c.DASHBOARDS_FOLDER, filepath))
80
+ self._copy_file(Path(c.DASHBOARDS_FOLDER, filepath))
81
81
 
82
82
  def _create_manifest_file(self, has_connections: bool, has_parameters: bool):
83
83
  def get_content(file_name: Optional[str]) -> str:
84
84
  if file_name is None:
85
85
  return ""
86
86
 
87
- yaml_path = u.Path(base_proj_dir, file_name)
87
+ yaml_path = Path(base_proj_dir, file_name)
88
88
  return yaml_path.read_text()
89
89
 
90
90
  file_name_dict = {
@@ -95,10 +95,10 @@ class Initializer:
95
95
 
96
96
  manifest_template = get_content(c.MANIFEST_JINJA_FILE)
97
97
  manifest_content = u.render_string(manifest_template, **substitutions)
98
- output_path = u.Path(base_proj_dir, TMP_FOLDER, c.MANIFEST_FILE)
98
+ output_path = Path(base_proj_dir, TMP_FOLDER, c.MANIFEST_FILE)
99
99
  output_path.write_text(manifest_content)
100
100
 
101
- self._copy_file(u.Path(c.MANIFEST_FILE), src_folder=TMP_FOLDER)
101
+ self._copy_file(Path(c.MANIFEST_FILE), src_folder=TMP_FOLDER)
102
102
 
103
103
  def _copy_dotenv_files(self, admin_password: str | None = None):
104
104
  substitutions = {
@@ -106,52 +106,76 @@ class Initializer:
106
106
  "random_admin_password": admin_password if admin_password else secrets.token_urlsafe(8),
107
107
  }
108
108
 
109
- dotenv_path = u.Path(base_proj_dir, c.DOTENV_FILE)
109
+ dotenv_path = Path(base_proj_dir, c.DOTENV_FILE)
110
110
  contents = u.render_string(dotenv_path.read_text(), **substitutions)
111
111
 
112
- output_path = u.Path(base_proj_dir, TMP_FOLDER, c.DOTENV_FILE)
112
+ output_path = Path(base_proj_dir, TMP_FOLDER, c.DOTENV_FILE)
113
113
  output_path.write_text(contents)
114
114
 
115
- self._copy_file(u.Path(c.DOTENV_FILE), src_folder=TMP_FOLDER)
116
- self._copy_file(u.Path(c.DOTENV_FILE + ".example"))
115
+ self._copy_file(Path(c.DOTENV_FILE), src_folder=TMP_FOLDER)
116
+ self._copy_file(Path(c.DOTENV_FILE + ".example"))
117
+
118
+ def _copy_gitignore_file(self):
119
+ self._copy_file(Path(c.GITIGNORE_FILE), src_file=Path("gitignore"))
117
120
 
118
121
  def init_project(self, args):
119
- options = ["core", "connections", "parameters", "build", "federate", "dashboard"]
120
- _, CONNECTIONS, PARAMETERS, BUILD, FEDERATE, DASHBOARD = options
122
+ options = ["connections", "parameters", "build", "federate", "dashboard", "admin_password"]
123
+ CONNECTIONS, PARAMETERS, BUILD, FEDERATE, DASHBOARD, ADMIN_PASSWORD = options
121
124
 
122
125
  # Add project name prompt if not provided
123
- if self.project_name is None:
126
+ if self.project_name is None and not args.curr_dir:
124
127
  questions = [
125
- inquirer.Text('project_name', message="What is your project name? (leave blank to create in current directory)")
128
+ inquirer.Text('project_name', message="What is your project folder name? (leave blank to create in current directory)")
126
129
  ]
127
130
  answers = inquirer.prompt(questions)
128
131
  assert isinstance(answers, dict)
129
132
  self.project_name = answers['project_name']
130
133
 
131
134
  answers = { x: getattr(args, x) for x in options }
132
- if not any(answers.values()):
133
- questions = [
134
- inquirer.List(
135
- CONNECTIONS, message=f"How would you like to configure the database connections?", choices=c.CONF_FORMAT_CHOICES
136
- ),
137
- inquirer.List(
138
- PARAMETERS, message=f"How would you like to configure the parameters?", choices=c.CONF_FORMAT_CHOICES2
139
- ),
140
- inquirer.List(
141
- BUILD, message="What's the file format for the build model?", choices=c.FILE_TYPE_CHOICES
142
- ),
143
- inquirer.List(
144
- FEDERATE, message="What's the file format for the federated model?", choices=c.FILE_TYPE_CHOICES
145
- ),
146
- inquirer.Confirm(
147
- DASHBOARD, message=f"Do you want to include a dashboard example?", default=False
148
- ),
149
- inquirer.Password(
150
- "admin_password", message="What's the admin password? (leave blank to generate a random one)"
151
- ),
152
- ]
153
- answers = inquirer.prompt(questions)
154
- assert isinstance(answers, dict)
135
+ if answers.get(DASHBOARD) is not None:
136
+ answers[DASHBOARD] = (answers[DASHBOARD] == 'y') # convert 'y' or 'n' to boolean
137
+
138
+ if not args.use_defaults:
139
+ questions = []
140
+ if answers.get(CONNECTIONS) is None:
141
+ questions.append(
142
+ inquirer.List(
143
+ CONNECTIONS, message=f"How would you like to configure the database connections?", choices=c.CONF_FORMAT_CHOICES
144
+ ),
145
+ )
146
+ if answers.get(PARAMETERS) is None:
147
+ questions.append(
148
+ inquirer.List(
149
+ PARAMETERS, message=f"How would you like to configure the parameters?", choices=c.CONF_FORMAT_CHOICES2
150
+ ),
151
+ )
152
+ if answers.get(BUILD) is None:
153
+ questions.append(
154
+ inquirer.List(
155
+ BUILD, message="What's the file format for the build model?", choices=c.FILE_TYPE_CHOICES
156
+ ),
157
+ )
158
+ if answers.get(FEDERATE) is None:
159
+ questions.append(
160
+ inquirer.List(
161
+ FEDERATE, message="What's the file format for the federated model?", choices=c.FILE_TYPE_CHOICES
162
+ ),
163
+ )
164
+ if answers.get(DASHBOARD) is None:
165
+ questions.append(
166
+ inquirer.Confirm(
167
+ DASHBOARD, message=f"Do you want to include a dashboard example?", default=False
168
+ ),
169
+ )
170
+ if answers.get(ADMIN_PASSWORD) is None:
171
+ questions.append(
172
+ inquirer.Password(
173
+ "admin_password", message="What's the admin password? (leave blank to generate a random one)"
174
+ ),
175
+ )
176
+ more_answers = inquirer.prompt(questions)
177
+ assert isinstance(more_answers, dict)
178
+ answers.update(more_answers)
155
179
 
156
180
  def get_answer(key, default):
157
181
  """
@@ -177,7 +201,7 @@ class Initializer:
177
201
  parameters_use_py = (parameters_format == c.PYTHON_FORMAT)
178
202
 
179
203
  build_config_file = c.BUILD_FILE_STEM + ".yml"
180
- build_format = get_answer(BUILD, c.PYTHON_FILE_TYPE)
204
+ build_format = get_answer(BUILD, c.SQL_FILE_TYPE)
181
205
  if build_format == c.SQL_FILE_TYPE:
182
206
  build_file = c.BUILD_FILE_STEM + ".sql"
183
207
  elif build_format == c.PYTHON_FILE_TYPE:
@@ -202,7 +226,7 @@ class Initializer:
202
226
  self._copy_dotenv_files(admin_password)
203
227
  self._create_manifest_file(connections_use_yaml, parameters_use_yaml)
204
228
 
205
- self._copy_file(u.Path(c.GITIGNORE_FILE))
229
+ self._copy_gitignore_file()
206
230
 
207
231
  if connections_use_py:
208
232
  self._copy_pyconfig_file(c.CONNECTIONS_FILE)
@@ -242,7 +266,7 @@ class Initializer:
242
266
 
243
267
  self._copy_database_file(c.EXPENSES_DB)
244
268
 
245
- print(f"\nSuccessfully created new Squirrels project in current directory!\n")
269
+ print(f"\nSuccessfully created new Squirrels project!\n")
246
270
 
247
271
  def get_file(self, args):
248
272
  if args.file_name == c.DOTENV_FILE:
@@ -253,7 +277,7 @@ class Initializer:
253
277
  print(f"You may also run `sqrl get-file {c.GITIGNORE_FILE}` to add a sample {c.GITIGNORE_FILE} file to your project.")
254
278
  print()
255
279
  elif args.file_name == c.GITIGNORE_FILE:
256
- self._copy_file(u.Path(c.GITIGNORE_FILE))
280
+ self._copy_gitignore_file()
257
281
  elif args.file_name == c.MANIFEST_FILE:
258
282
  self._create_manifest_file(not args.no_connections, args.parameters)
259
283
  elif args.file_name in (c.USER_FILE, c.CONNECTIONS_FILE, c.PARAMETERS_FILE, c.CONTEXT_FILE):
squirrels/_logging.py ADDED
@@ -0,0 +1,117 @@
1
+ from pathlib import Path
2
+ from logging.handlers import RotatingFileHandler
3
+ from uuid import uuid4
4
+ import logging as l, json
5
+
6
+ from . import _constants as c, _utils as u
7
+ from ._request_context import get_request_id
8
+
9
+
10
+ class _BaseFormatter(l.Formatter):
11
+ def _format_helper(self, level_for_print: str, record: l.LogRecord) -> str:
12
+ # Save original levelname
13
+ original_levelname = record.levelname
14
+
15
+ # Add padding to the levelname for printing
16
+ visible_length = len(record.levelname) + 1
17
+ padding_needed = max(1, 9 - visible_length)
18
+ padded_level = f"{level_for_print}:{' ' * padding_needed}"
19
+ record.levelname = padded_level
20
+
21
+ # Format the message
22
+ formatted = super().format(record)
23
+
24
+ # Append request ID if available
25
+ request_id = get_request_id()
26
+ request_id_str = f" [req_id: {request_id}]" if request_id else ""
27
+ formatted = formatted.format(request_id=request_id_str)
28
+
29
+ # Restore original levelname
30
+ record.levelname = original_levelname
31
+
32
+ return formatted
33
+
34
+
35
+ class _ColoredFormatter(_BaseFormatter):
36
+ """Custom formatter that adds colors to log levels for terminal output"""
37
+
38
+ # ANSI color codes
39
+ COLORS = {
40
+ 'DEBUG': '\033[36m', # Cyan
41
+ 'INFO': '\033[32m', # Green
42
+ 'WARNING': '\033[33m', # Yellow
43
+ 'ERROR': '\033[31m', # Red
44
+ 'CRITICAL': '\033[35m', # Magenta
45
+ }
46
+ RESET = '\033[0m'
47
+ BOLD = '\033[1m'
48
+
49
+ def format(self, record: l.LogRecord) -> str:
50
+ # Add color to levelname with colon and padding
51
+ color = self.COLORS.get(record.levelname, '')
52
+ colored_level = f"{color}{record.levelname}{self.RESET}"
53
+ return self._format_helper(colored_level, record)
54
+
55
+
56
+ class _PlainFormatter(_BaseFormatter):
57
+ """Custom formatter that adds colon to log levels for file output"""
58
+
59
+ def format(self, record: l.LogRecord) -> str:
60
+ return self._format_helper(record.levelname, record)
61
+
62
+
63
+ class _CustomJsonFormatter(l.Formatter):
64
+ def format(self, record: l.LogRecord) -> str:
65
+ super().format(record)
66
+ request_id = get_request_id()
67
+ info = {
68
+ "timestamp": self.formatTime(record),
69
+ "level": record.levelname,
70
+ "message": record.getMessage(),
71
+ "request_id": request_id,
72
+ }
73
+ output = {
74
+ "data": record.__dict__.get("data", {}),
75
+ "info": info
76
+ }
77
+ return json.dumps(output)
78
+
79
+
80
+ def get_logger(
81
+ base_path: str, log_to_file: bool, log_level: str, log_format: str, log_file_size_mb: int, log_file_backup_count: int
82
+ ) -> u.Logger:
83
+ logger = u.Logger(name=uuid4().hex, level=log_level.upper())
84
+
85
+ # Determine the formatter based on log_format
86
+ if log_format.lower() == "json":
87
+ stdout_formatter = _CustomJsonFormatter()
88
+ file_formatter = _CustomJsonFormatter()
89
+ elif log_format.lower() == "text":
90
+ # Use colored formatter for stdout, plain formatter with colon for file
91
+ format_string = "%(levelname)s [%(asctime)s]{request_id} %(message)s"
92
+ stdout_formatter = _ColoredFormatter(format_string, datefmt="%Y-%m-%d %H:%M:%S")
93
+ file_formatter = _PlainFormatter(format_string, datefmt="%Y-%m-%d %H:%M:%S")
94
+ else:
95
+ raise ValueError("log_format must be either 'text' or 'json'")
96
+
97
+ if log_to_file:
98
+ log_file_path = Path(base_path, c.LOGS_FOLDER, c.LOGS_FILE)
99
+ log_file_path.parent.mkdir(parents=True, exist_ok=True)
100
+
101
+ # Rotating file handler
102
+ file_handler = RotatingFileHandler(
103
+ log_file_path,
104
+ maxBytes=log_file_size_mb * 1024 * 1024,
105
+ backupCount=log_file_backup_count
106
+ )
107
+ file_handler.setLevel(log_level.upper())
108
+ file_handler.setFormatter(file_formatter)
109
+ logger.addHandler(file_handler)
110
+
111
+ else:
112
+ stdout_handler = l.StreamHandler()
113
+ stdout_handler.setLevel(log_level.upper())
114
+ stdout_handler.setFormatter(stdout_formatter)
115
+ logger.addHandler(stdout_handler)
116
+
117
+ return logger