squirrels 0.5.0b4__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 (69) hide show
  1. squirrels/__init__.py +2 -0
  2. squirrels/_api_routes/auth.py +83 -74
  3. squirrels/_api_routes/base.py +58 -41
  4. squirrels/_api_routes/dashboards.py +37 -21
  5. squirrels/_api_routes/data_management.py +72 -27
  6. squirrels/_api_routes/datasets.py +107 -84
  7. squirrels/_api_routes/oauth2.py +11 -13
  8. squirrels/_api_routes/project.py +71 -33
  9. squirrels/_api_server.py +130 -63
  10. squirrels/_arguments/run_time_args.py +9 -9
  11. squirrels/_auth.py +117 -162
  12. squirrels/_command_line.py +68 -32
  13. squirrels/_compile_prompts.py +147 -0
  14. squirrels/_connection_set.py +11 -2
  15. squirrels/_constants.py +22 -8
  16. squirrels/_data_sources.py +38 -32
  17. squirrels/_dataset_types.py +2 -4
  18. squirrels/_initializer.py +1 -1
  19. squirrels/_logging.py +117 -0
  20. squirrels/_manifest.py +125 -58
  21. squirrels/_model_builder.py +10 -54
  22. squirrels/_models.py +224 -108
  23. squirrels/_package_data/base_project/.env +15 -4
  24. squirrels/_package_data/base_project/.env.example +14 -3
  25. squirrels/_package_data/base_project/connections.yml +4 -3
  26. squirrels/_package_data/base_project/dashboards/dashboard_example.py +2 -2
  27. squirrels/_package_data/base_project/dashboards/dashboard_example.yml +4 -4
  28. squirrels/_package_data/base_project/duckdb_init.sql +1 -0
  29. squirrels/_package_data/base_project/models/dbviews/dbview_example.sql +7 -2
  30. squirrels/_package_data/base_project/models/dbviews/dbview_example.yml +16 -10
  31. squirrels/_package_data/base_project/models/federates/federate_example.py +22 -15
  32. squirrels/_package_data/base_project/models/federates/federate_example.sql +3 -7
  33. squirrels/_package_data/base_project/models/federates/federate_example.yml +1 -1
  34. squirrels/_package_data/base_project/models/sources.yml +5 -6
  35. squirrels/_package_data/base_project/parameters.yml +24 -38
  36. squirrels/_package_data/base_project/pyconfigs/connections.py +5 -1
  37. squirrels/_package_data/base_project/pyconfigs/context.py +23 -12
  38. squirrels/_package_data/base_project/pyconfigs/parameters.py +68 -33
  39. squirrels/_package_data/base_project/pyconfigs/user.py +11 -18
  40. squirrels/_package_data/base_project/seeds/seed_categories.yml +1 -1
  41. squirrels/_package_data/base_project/seeds/seed_subcategories.yml +1 -1
  42. squirrels/_package_data/base_project/squirrels.yml.j2 +18 -28
  43. squirrels/_package_data/templates/squirrels_studio.html +20 -0
  44. squirrels/_parameter_configs.py +43 -22
  45. squirrels/_parameter_options.py +1 -1
  46. squirrels/_parameter_sets.py +8 -10
  47. squirrels/_project.py +351 -234
  48. squirrels/_request_context.py +33 -0
  49. squirrels/_schemas/auth_models.py +32 -9
  50. squirrels/_schemas/query_param_models.py +9 -1
  51. squirrels/_schemas/response_models.py +36 -10
  52. squirrels/_seeds.py +1 -1
  53. squirrels/_sources.py +23 -19
  54. squirrels/_utils.py +83 -35
  55. squirrels/_version.py +1 -1
  56. squirrels/arguments.py +5 -0
  57. squirrels/auth.py +4 -1
  58. squirrels/connections.py +2 -0
  59. squirrels/dashboards.py +3 -1
  60. squirrels/data_sources.py +6 -0
  61. squirrels/parameter_options.py +5 -0
  62. squirrels/parameters.py +5 -0
  63. squirrels/types.py +6 -1
  64. {squirrels-0.5.0b4.dist-info → squirrels-0.5.1.dist-info}/METADATA +28 -13
  65. squirrels-0.5.1.dist-info/RECORD +98 -0
  66. squirrels-0.5.0b4.dist-info/RECORD +0 -94
  67. {squirrels-0.5.0b4.dist-info → squirrels-0.5.1.dist-info}/WHEEL +0 -0
  68. {squirrels-0.5.0b4.dist-info → squirrels-0.5.1.dist-info}/entry_points.txt +0 -0
  69. {squirrels-0.5.0b4.dist-info → squirrels-0.5.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,10 +1,16 @@
1
1
  from __future__ import annotations
2
2
  from dataclasses import dataclass
3
+ from enum import Enum
3
4
  import polars as pl, typing as t, abc
4
5
 
5
6
  from . import _parameter_configs as pc, _parameter_options as po
6
7
  from ._exceptions import ConfigurationError
7
8
 
9
+ class SourceEnum(Enum):
10
+ CONNECTION = "connection"
11
+ SEEDS = "seeds"
12
+ VDL = "vdl"
13
+
8
14
 
9
15
  @dataclass
10
16
  class DataSource(metaclass=abc.ABCMeta):
@@ -13,19 +19,19 @@ class DataSource(metaclass=abc.ABCMeta):
13
19
  """
14
20
  _table_or_query: str
15
21
  _id_col: str | None
16
- _is_from_seeds: bool
22
+ _source: SourceEnum
17
23
  _user_group_col: str | None
18
24
  _parent_id_col: str | None
19
25
  _connection: str | None
20
26
 
21
27
  @abc.abstractmethod
22
28
  def __init__(
23
- self, table_or_query: str, *, id_col: str | None = None, from_seeds: bool = False, user_group_col: str | None = None,
24
- parent_id_col: str | None = None, connection: str | None = None, **kwargs
29
+ self, table_or_query: str, *, id_col: str | None = None, source: SourceEnum = SourceEnum.CONNECTION,
30
+ user_group_col: str | None = None, parent_id_col: str | None = None, connection: str | None = None, **kwargs
25
31
  ) -> None:
26
32
  self._table_or_query = table_or_query
27
33
  self._id_col = id_col
28
- self._is_from_seeds = from_seeds
34
+ self._source = source
29
35
  self._user_group_col = user_group_col
30
36
  self._parent_id_col = parent_id_col
31
37
  self._connection = connection
@@ -100,12 +106,12 @@ class _SelectionDataSource(DataSource):
100
106
  @abc.abstractmethod
101
107
  def __init__(
102
108
  self, table_or_query: str, id_col: str, options_col: str, *, order_by_col: str | None = None,
103
- is_default_col: str | None = None, custom_cols: dict[str, str] = {}, from_seeds: bool = False,
109
+ is_default_col: str | None = None, custom_cols: dict[str, str] = {}, source: SourceEnum = SourceEnum.CONNECTION,
104
110
  user_group_col: str | None = None, parent_id_col: str | None = None, connection: str | None = None,
105
111
  **kwargs
106
112
  ) -> None:
107
113
  super().__init__(
108
- table_or_query, id_col=id_col, from_seeds=from_seeds, user_group_col=user_group_col, parent_id_col=parent_id_col,
114
+ table_or_query, id_col=id_col, source=source, user_group_col=user_group_col, parent_id_col=parent_id_col,
109
115
  connection=connection
110
116
  )
111
117
  self._options_col = options_col
@@ -151,7 +157,7 @@ class SelectDataSource(_SelectionDataSource):
151
157
 
152
158
  def __init__(
153
159
  self, table_or_query: str, id_col: str, options_col: str, *, order_by_col: str | None = None,
154
- is_default_col: str | None = None, custom_cols: dict[str, str] = {}, from_seeds: bool = False,
160
+ is_default_col: str | None = None, custom_cols: dict[str, str] = {}, source: SourceEnum = SourceEnum.CONNECTION,
155
161
  user_group_col: str | None = None, parent_id_col: str | None = None, connection: str | None = None,
156
162
  **kwargs
157
163
  ) -> None:
@@ -165,19 +171,19 @@ class SelectDataSource(_SelectionDataSource):
165
171
  order_by_col: The column name to order the options by. Orders by the id_col instead if this is None
166
172
  is_default_col: The column name that indicates which options are the default
167
173
  custom_cols: Dictionary of attribute to column name for custom fields for the SelectParameterOption
168
- from_seeds: Boolean for whether this datasource is created from seeds
174
+ source: The source to fetch data from. Must be "connection", "seeds", or "vdl". Defaults to "connection"
169
175
  user_group_col: The column name of the user group that the user is in for this option to be valid
170
176
  parent_id_col: The column name of the parent option id that must be selected for this option to be valid
171
177
  connection: Name of the connection to use defined in connections.py
172
178
  """
173
179
  super().__init__(
174
180
  table_or_query, id_col, options_col, order_by_col=order_by_col, is_default_col=is_default_col, custom_cols=custom_cols,
175
- from_seeds=from_seeds, user_group_col=user_group_col, parent_id_col=parent_id_col, connection=connection
181
+ source=source, user_group_col=user_group_col, parent_id_col=parent_id_col, connection=connection
176
182
  )
177
183
 
178
184
  def _convert(self, ds_param: pc.DataSourceParameterConfig, df: pl.DataFrame) -> pc.SelectionParameterConfig:
179
185
  """
180
- Method to convert the associated DataSourceParameter into a SingleSelectParameterConfig or MultiSelectParameterConfig
186
+ Method to convert the associated DataSourceParameterConfig into a SingleSelectParameterConfig or MultiSelectParameterConfig
181
187
 
182
188
  Arguments:
183
189
  ds_param: The parameter to convert
@@ -212,7 +218,7 @@ class DateDataSource(DataSource):
212
218
  def __init__(
213
219
  self, table_or_query: str, default_date_col: str, *, min_date_col: str | None = None,
214
220
  max_date_col: str | None = None, date_format: str = '%Y-%m-%d', id_col: str | None = None,
215
- from_seeds: bool = False, user_group_col: str | None = None, parent_id_col: str | None = None,
221
+ source: SourceEnum = SourceEnum.CONNECTION, user_group_col: str | None = None, parent_id_col: str | None = None,
216
222
  connection: str | None = None, **kwargs
217
223
  ) -> None:
218
224
  """
@@ -223,13 +229,13 @@ class DateDataSource(DataSource):
223
229
  default_date_col: The column name of the default date
224
230
  date_format: The format of the default date(s). Defaults to '%Y-%m-%d'
225
231
  id_col: The column name of the id
226
- from_seeds: Boolean for whether this datasource is created from seeds
232
+ source: The source to fetch data from. Must be "connection", "seeds", or "vdl". Defaults to "connection"
227
233
  user_group_col: The column name of the user group that the user is in for this option to be valid
228
234
  parent_id_col: The column name of the parent option id that the default date belongs to
229
235
  connection: Name of the connection to use defined in connections.py
230
236
  """
231
237
  super().__init__(
232
- table_or_query, id_col=id_col, from_seeds=from_seeds, user_group_col=user_group_col, parent_id_col=parent_id_col,
238
+ table_or_query, id_col=id_col, source=source, user_group_col=user_group_col, parent_id_col=parent_id_col,
233
239
  connection=connection
234
240
  )
235
241
  self._default_date_col = default_date_col
@@ -239,7 +245,7 @@ class DateDataSource(DataSource):
239
245
 
240
246
  def _convert(self, ds_param: pc.DataSourceParameterConfig, df: pl.DataFrame) -> pc.DateParameterConfig:
241
247
  """
242
- Method to convert the associated DataSourceParameter into a DateParameterConfig
248
+ Method to convert the associated DataSourceParameterConfig into a DateParameterConfig
243
249
 
244
250
  Arguments:
245
251
  ds_param: The parameter to convert
@@ -281,7 +287,7 @@ class DateRangeDataSource(DataSource):
281
287
 
282
288
  def __init__(
283
289
  self, table_or_query: str, default_start_date_col: str, default_end_date_col: str, *, date_format: str = '%Y-%m-%d',
284
- min_date_col: str | None = None, max_date_col: str | None = None, id_col: str | None = None, from_seeds: bool = False,
290
+ min_date_col: str | None = None, max_date_col: str | None = None, id_col: str | None = None, source: SourceEnum = SourceEnum.CONNECTION,
285
291
  user_group_col: str | None = None, parent_id_col: str | None = None, connection: str | None = None, **kwargs
286
292
  ) -> None:
287
293
  """
@@ -293,13 +299,13 @@ class DateRangeDataSource(DataSource):
293
299
  default_end_date_col: The column name of the default end date
294
300
  date_format: The format of the default date(s). Defaults to '%Y-%m-%d'
295
301
  id_col: The column name of the id
296
- from_seeds: Boolean for whether this datasource is created from seeds
302
+ source: The source to fetch data from. Must be "connection", "seeds", or "vdl". Defaults to "connection"
297
303
  user_group_col: The column name of the user group that the user is in for this option to be valid
298
304
  parent_id_col: The column name of the parent option id that the default date belongs to
299
305
  connection: Name of the connection to use defined in connections.py
300
306
  """
301
307
  super().__init__(
302
- table_or_query, id_col=id_col, from_seeds=from_seeds, user_group_col=user_group_col, parent_id_col=parent_id_col,
308
+ table_or_query, id_col=id_col, source=source, user_group_col=user_group_col, parent_id_col=parent_id_col,
303
309
  connection=connection
304
310
  )
305
311
  self._default_start_date_col = default_start_date_col
@@ -310,7 +316,7 @@ class DateRangeDataSource(DataSource):
310
316
 
311
317
  def _convert(self, ds_param: pc.DataSourceParameterConfig, df: pl.DataFrame) -> pc.DateRangeParameterConfig:
312
318
  """
313
- Method to convert the associated DataSourceParameter into a DateRangeParameterConfig
319
+ Method to convert the associated DataSourceParameterConfig into a DateRangeParameterConfig
314
320
 
315
321
  Arguments:
316
322
  ds_param: The parameter to convert
@@ -354,11 +360,11 @@ class _NumericDataSource(DataSource):
354
360
  @abc.abstractmethod
355
361
  def __init__(
356
362
  self, table_or_query: str, min_value_col: str, max_value_col: str, *, increment_col: str | None = None,
357
- id_col: str | None = None, from_seeds: bool = False, user_group_col: str | None = None,
363
+ id_col: str | None = None, source: SourceEnum = SourceEnum.CONNECTION, user_group_col: str | None = None,
358
364
  parent_id_col: str | None = None, connection: str | None = None, **kwargs
359
365
  ) -> None:
360
366
  super().__init__(
361
- table_or_query, id_col=id_col, from_seeds=from_seeds, user_group_col=user_group_col, parent_id_col=parent_id_col,
367
+ table_or_query, id_col=id_col, source=source, user_group_col=user_group_col, parent_id_col=parent_id_col,
362
368
  connection=connection
363
369
  )
364
370
  self._min_value_col = min_value_col
@@ -375,7 +381,7 @@ class NumberDataSource(_NumericDataSource):
375
381
 
376
382
  def __init__(
377
383
  self, table_or_query: str, min_value_col: str, max_value_col: str, *, increment_col: str | None = None,
378
- default_value_col: str | None = None, id_col: str | None = None, from_seeds: bool = False,
384
+ default_value_col: str | None = None, id_col: str | None = None, source: SourceEnum = SourceEnum.CONNECTION,
379
385
  user_group_col: str | None = None, parent_id_col: str | None = None, connection: str | None = None, **kwargs
380
386
  ) -> None:
381
387
  """
@@ -388,20 +394,20 @@ class NumberDataSource(_NumericDataSource):
388
394
  increment_col: The column name of the increment value. Defaults to column of 1's if None
389
395
  default_value_col: The column name of the default value. Defaults to min_value_col if None
390
396
  id_col: The column name of the id
391
- from_seeds: Boolean for whether this datasource is created from seeds
397
+ source: The source to fetch data from. Must be "connection", "seeds", or "vdl". Defaults to "connection"
392
398
  user_group_col: The column name of the user group that the user is in for this option to be valid
393
399
  parent_id_col: The column name of the parent option id that the default value belongs to
394
400
  connection: Name of the connection to use defined in connections.py
395
401
  """
396
402
  super().__init__(
397
- table_or_query, min_value_col, max_value_col, increment_col=increment_col, id_col=id_col, from_seeds=from_seeds,
403
+ table_or_query, min_value_col, max_value_col, increment_col=increment_col, id_col=id_col, source=source,
398
404
  user_group_col=user_group_col, parent_id_col=parent_id_col, connection=connection
399
405
  )
400
406
  self._default_value_col = default_value_col
401
407
 
402
408
  def _convert(self, ds_param: pc.DataSourceParameterConfig, df: pl.DataFrame) -> pc.NumberParameterConfig:
403
409
  """
404
- Method to convert the associated DataSourceParameter into a NumberParameterConfig
410
+ Method to convert the associated DataSourceParameterConfig into a NumberParameterConfig
405
411
 
406
412
  Arguments:
407
413
  ds_param: The parameter to convert
@@ -443,7 +449,7 @@ class NumberRangeDataSource(_NumericDataSource):
443
449
  def __init__(
444
450
  self, table_or_query: str, min_value_col: str, max_value_col: str, *, increment_col: str | None = None,
445
451
  default_lower_value_col: str | None = None, default_upper_value_col: str | None = None, id_col: str | None = None,
446
- from_seeds: bool = False, user_group_col: str | None = None, parent_id_col: str | None = None,
452
+ source: SourceEnum = SourceEnum.CONNECTION, user_group_col: str | None = None, parent_id_col: str | None = None,
447
453
  connection: str | None = None, **kwargs
448
454
  ) -> None:
449
455
  """
@@ -457,13 +463,13 @@ class NumberRangeDataSource(_NumericDataSource):
457
463
  default_lower_value_col: The column name of the default lower value. Defaults to min_value_col if None
458
464
  default_upper_value_col: The column name of the default upper value. Defaults to max_value_col if None
459
465
  id_col: The column name of the id
460
- from_seeds: Boolean for whether this datasource is created from seeds
466
+ source: The source to fetch data from. Must be "connection", "seeds", or "vdl". Defaults to "connection"
461
467
  user_group_col: The column name of the user group that the user is in for this option to be valid
462
468
  parent_id_col: The column name of the parent option id that the default value belongs to
463
469
  connection: Name of the connection to use defined in connections.py
464
470
  """
465
471
  super().__init__(
466
- table_or_query, min_value_col, max_value_col, increment_col=increment_col, id_col=id_col, from_seeds=from_seeds,
472
+ table_or_query, min_value_col, max_value_col, increment_col=increment_col, id_col=id_col, source=source,
467
473
  user_group_col=user_group_col, parent_id_col=parent_id_col, connection=connection
468
474
  )
469
475
  self._default_lower_value_col = default_lower_value_col
@@ -471,7 +477,7 @@ class NumberRangeDataSource(_NumericDataSource):
471
477
 
472
478
  def _convert(self, ds_param: pc.DataSourceParameterConfig, df: pl.DataFrame) -> pc.NumberRangeParameterConfig:
473
479
  """
474
- Method to convert the associated DataSourceParameter into a NumberRangeParameterConfig
480
+ Method to convert the associated DataSourceParameterConfig into a NumberRangeParameterConfig
475
481
 
476
482
  Arguments:
477
483
  ds_param: The parameter to convert
@@ -511,7 +517,7 @@ class TextDataSource(DataSource):
511
517
  _default_text_col: str
512
518
 
513
519
  def __init__(
514
- self, table_or_query: str, default_text_col: str, *, id_col: str | None = None, from_seeds: bool = False,
520
+ self, table_or_query: str, default_text_col: str, *, id_col: str | None = None, source: SourceEnum = SourceEnum.CONNECTION,
515
521
  user_group_col: str | None = None, parent_id_col: str | None = None, connection: str | None = None,
516
522
  **kwargs
517
523
  ) -> None:
@@ -522,20 +528,20 @@ class TextDataSource(DataSource):
522
528
  table_or_query: Either the name of the table to use, or a query to run
523
529
  default_text_col: The column name of the default text
524
530
  id_col: The column name of the id
525
- from_seeds: Boolean for whether this datasource is created from seeds
531
+ source: The source to fetch data from. Must be "connection", "seeds", or "vdl". Defaults to "connection"
526
532
  user_group_col: The column name of the user group that the user is in for this option to be valid
527
533
  parent_id_col: The column name of the parent option id that the default date belongs to
528
534
  connection: Name of the connection to use defined in connections.py
529
535
  """
530
536
  super().__init__(
531
- table_or_query, id_col=id_col, from_seeds=from_seeds, user_group_col=user_group_col, parent_id_col=parent_id_col,
537
+ table_or_query, id_col=id_col, source=source, user_group_col=user_group_col, parent_id_col=parent_id_col,
532
538
  connection=connection
533
539
  )
534
540
  self._default_text_col = default_text_col
535
541
 
536
542
  def _convert(self, ds_param: pc.DataSourceParameterConfig, df: pl.DataFrame) -> pc.TextParameterConfig:
537
543
  """
538
- Method to convert the associated DataSourceParameter into a TextParameterConfig
544
+ Method to convert the associated DataSourceParameterConfig into a TextParameterConfig
539
545
 
540
546
  Arguments:
541
547
  ds_param: The parameter to convert
@@ -35,19 +35,17 @@ class DatasetMetadata:
35
35
  @dataclass
36
36
  class DatasetResult(DatasetMetadata):
37
37
  df: pl.DataFrame
38
- to_json: Callable[[str, tuple[str, ...], int, int], dict] = field(init=False)
38
+ to_json: Callable[[str, int, int], dict] = field(init=False)
39
39
 
40
40
  def __post_init__(self):
41
41
  self.to_json = lru_cache()(self._to_json)
42
42
 
43
- def _to_json(self, orientation: Literal["records", "rows", "columns"], select: tuple[str, ...], limit: int, offset: int) -> dict:
43
+ def _to_json(self, orientation: Literal["records", "rows", "columns"], limit: int, offset: int) -> dict:
44
44
  df = self.df.lazy()
45
45
  if offset > 0:
46
46
  df = df.filter(pl.col("_row_num") > offset)
47
47
  if limit > 0:
48
48
  df = df.limit(limit)
49
- if select:
50
- df = df.select(select)
51
49
  df = df.collect()
52
50
 
53
51
  if orientation == "columns":
squirrels/_initializer.py CHANGED
@@ -266,7 +266,7 @@ class Initializer:
266
266
 
267
267
  self._copy_database_file(c.EXPENSES_DB)
268
268
 
269
- print(f"\nSuccessfully created new Squirrels project in current directory!\n")
269
+ print(f"\nSuccessfully created new Squirrels project!\n")
270
270
 
271
271
  def get_file(self, args):
272
272
  if args.file_name == c.DOTENV_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