squirrels 0.1.0__py3-none-any.whl → 0.6.0.post0__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 (127) hide show
  1. dateutils/__init__.py +6 -0
  2. dateutils/_enums.py +25 -0
  3. squirrels/dateutils.py → dateutils/_implementation.py +409 -380
  4. dateutils/types.py +6 -0
  5. squirrels/__init__.py +21 -18
  6. squirrels/_api_routes/__init__.py +5 -0
  7. squirrels/_api_routes/auth.py +337 -0
  8. squirrels/_api_routes/base.py +196 -0
  9. squirrels/_api_routes/dashboards.py +156 -0
  10. squirrels/_api_routes/data_management.py +148 -0
  11. squirrels/_api_routes/datasets.py +220 -0
  12. squirrels/_api_routes/project.py +289 -0
  13. squirrels/_api_server.py +552 -134
  14. squirrels/_arguments/__init__.py +0 -0
  15. squirrels/_arguments/init_time_args.py +83 -0
  16. squirrels/_arguments/run_time_args.py +111 -0
  17. squirrels/_auth.py +777 -0
  18. squirrels/_command_line.py +239 -107
  19. squirrels/_compile_prompts.py +147 -0
  20. squirrels/_connection_set.py +94 -0
  21. squirrels/_constants.py +141 -64
  22. squirrels/_dashboards.py +179 -0
  23. squirrels/_data_sources.py +570 -0
  24. squirrels/_dataset_types.py +91 -0
  25. squirrels/_env_vars.py +209 -0
  26. squirrels/_exceptions.py +29 -0
  27. squirrels/_http_error_responses.py +52 -0
  28. squirrels/_initializer.py +319 -110
  29. squirrels/_logging.py +121 -0
  30. squirrels/_manifest.py +357 -187
  31. squirrels/_mcp_server.py +578 -0
  32. squirrels/_model_builder.py +69 -0
  33. squirrels/_model_configs.py +74 -0
  34. squirrels/_model_queries.py +52 -0
  35. squirrels/_models.py +1201 -0
  36. squirrels/_package_data/base_project/.env +7 -0
  37. squirrels/_package_data/base_project/.env.example +44 -0
  38. squirrels/_package_data/base_project/connections.yml +16 -0
  39. squirrels/_package_data/base_project/dashboards/dashboard_example.py +40 -0
  40. squirrels/_package_data/base_project/dashboards/dashboard_example.yml +22 -0
  41. squirrels/_package_data/base_project/docker/.dockerignore +16 -0
  42. squirrels/_package_data/base_project/docker/Dockerfile +16 -0
  43. squirrels/_package_data/base_project/docker/compose.yml +7 -0
  44. squirrels/_package_data/base_project/duckdb_init.sql +10 -0
  45. squirrels/_package_data/base_project/gitignore +13 -0
  46. squirrels/_package_data/base_project/macros/macros_example.sql +17 -0
  47. squirrels/_package_data/base_project/models/builds/build_example.py +26 -0
  48. squirrels/_package_data/base_project/models/builds/build_example.sql +16 -0
  49. squirrels/_package_data/base_project/models/builds/build_example.yml +57 -0
  50. squirrels/_package_data/base_project/models/dbviews/dbview_example.sql +17 -0
  51. squirrels/_package_data/base_project/models/dbviews/dbview_example.yml +32 -0
  52. squirrels/_package_data/base_project/models/federates/federate_example.py +51 -0
  53. squirrels/_package_data/base_project/models/federates/federate_example.sql +21 -0
  54. squirrels/_package_data/base_project/models/federates/federate_example.yml +65 -0
  55. squirrels/_package_data/base_project/models/sources.yml +38 -0
  56. squirrels/_package_data/base_project/parameters.yml +142 -0
  57. squirrels/_package_data/base_project/pyconfigs/connections.py +19 -0
  58. squirrels/_package_data/base_project/pyconfigs/context.py +96 -0
  59. squirrels/_package_data/base_project/pyconfigs/parameters.py +141 -0
  60. squirrels/_package_data/base_project/pyconfigs/user.py +56 -0
  61. squirrels/_package_data/base_project/resources/expenses.db +0 -0
  62. squirrels/_package_data/base_project/resources/public/.gitkeep +0 -0
  63. squirrels/_package_data/base_project/resources/weather.db +0 -0
  64. squirrels/_package_data/base_project/seeds/seed_categories.csv +6 -0
  65. squirrels/_package_data/base_project/seeds/seed_categories.yml +15 -0
  66. squirrels/_package_data/base_project/seeds/seed_subcategories.csv +15 -0
  67. squirrels/_package_data/base_project/seeds/seed_subcategories.yml +21 -0
  68. squirrels/_package_data/base_project/squirrels.yml.j2 +61 -0
  69. squirrels/_package_data/base_project/tmp/.gitignore +2 -0
  70. squirrels/_package_data/templates/login_successful.html +53 -0
  71. squirrels/_package_data/templates/squirrels_studio.html +22 -0
  72. squirrels/_package_loader.py +29 -0
  73. squirrels/_parameter_configs.py +592 -0
  74. squirrels/_parameter_options.py +348 -0
  75. squirrels/_parameter_sets.py +207 -0
  76. squirrels/_parameters.py +1703 -0
  77. squirrels/_project.py +796 -0
  78. squirrels/_py_module.py +122 -0
  79. squirrels/_request_context.py +33 -0
  80. squirrels/_schemas/__init__.py +0 -0
  81. squirrels/_schemas/auth_models.py +83 -0
  82. squirrels/_schemas/query_param_models.py +70 -0
  83. squirrels/_schemas/request_models.py +26 -0
  84. squirrels/_schemas/response_models.py +286 -0
  85. squirrels/_seeds.py +97 -0
  86. squirrels/_sources.py +112 -0
  87. squirrels/_utils.py +540 -149
  88. squirrels/_version.py +1 -3
  89. squirrels/arguments.py +7 -0
  90. squirrels/auth.py +4 -0
  91. squirrels/connections.py +3 -0
  92. squirrels/dashboards.py +3 -0
  93. squirrels/data_sources.py +14 -282
  94. squirrels/parameter_options.py +13 -189
  95. squirrels/parameters.py +14 -801
  96. squirrels/types.py +18 -0
  97. squirrels-0.6.0.post0.dist-info/METADATA +148 -0
  98. squirrels-0.6.0.post0.dist-info/RECORD +101 -0
  99. {squirrels-0.1.0.dist-info → squirrels-0.6.0.post0.dist-info}/WHEEL +1 -2
  100. {squirrels-0.1.0.dist-info → squirrels-0.6.0.post0.dist-info}/entry_points.txt +1 -0
  101. squirrels-0.6.0.post0.dist-info/licenses/LICENSE +201 -0
  102. squirrels/_credentials_manager.py +0 -87
  103. squirrels/_module_loader.py +0 -37
  104. squirrels/_parameter_set.py +0 -151
  105. squirrels/_renderer.py +0 -286
  106. squirrels/_timed_imports.py +0 -37
  107. squirrels/connection_set.py +0 -126
  108. squirrels/package_data/base_project/.gitignore +0 -4
  109. squirrels/package_data/base_project/connections.py +0 -21
  110. squirrels/package_data/base_project/database/sample_database.db +0 -0
  111. squirrels/package_data/base_project/database/seattle_weather.db +0 -0
  112. squirrels/package_data/base_project/datasets/sample_dataset/context.py +0 -8
  113. squirrels/package_data/base_project/datasets/sample_dataset/database_view1.py +0 -23
  114. squirrels/package_data/base_project/datasets/sample_dataset/database_view1.sql.j2 +0 -7
  115. squirrels/package_data/base_project/datasets/sample_dataset/final_view.py +0 -10
  116. squirrels/package_data/base_project/datasets/sample_dataset/final_view.sql.j2 +0 -2
  117. squirrels/package_data/base_project/datasets/sample_dataset/parameters.py +0 -30
  118. squirrels/package_data/base_project/datasets/sample_dataset/selections.cfg +0 -6
  119. squirrels/package_data/base_project/squirrels.yaml +0 -26
  120. squirrels/package_data/static/favicon.ico +0 -0
  121. squirrels/package_data/static/script.js +0 -234
  122. squirrels/package_data/static/style.css +0 -110
  123. squirrels/package_data/templates/index.html +0 -32
  124. squirrels-0.1.0.dist-info/LICENSE +0 -22
  125. squirrels-0.1.0.dist-info/METADATA +0 -67
  126. squirrels-0.1.0.dist-info/RECORD +0 -40
  127. squirrels-0.1.0.dist-info/top_level.txt +0 -1
squirrels/_initializer.py CHANGED
@@ -1,110 +1,319 @@
1
- import inquirer, os, shutil
2
-
3
- from squirrels import _constants as c, _utils
4
- from squirrels._version import major_version
5
-
6
- base_proj_dir = _utils.join_paths(os.path.dirname(__file__), 'package_data', 'base_project')
7
- dataset_dir = _utils.join_paths('datasets', 'sample_dataset')
8
-
9
-
10
- class Initializer:
11
- def __init__(self, overwrite: bool):
12
- self.overwrite = overwrite
13
-
14
- def _path_exists(self, filepath: str) -> bool:
15
- if not self.overwrite and os.path.exists(filepath):
16
- print(f'File "{filepath}" already exists. Creation skipped.')
17
- return True
18
- return False
19
-
20
- def _copy_file(self, filepath: str):
21
- if not self._path_exists(filepath):
22
- dest_dir = os.path.dirname(filepath)
23
- if dest_dir != '':
24
- os.makedirs(dest_dir, exist_ok=True)
25
- src_path = _utils.join_paths(base_proj_dir, filepath)
26
- shutil.copy(src_path, filepath)
27
-
28
- def _copy_dataset_file(self, filepath: str):
29
- self._copy_file(_utils.join_paths(dataset_dir, filepath))
30
-
31
- def _copy_database_file(self, filepath: str):
32
- self._copy_file(_utils.join_paths('database', filepath))
33
-
34
- def _create_requirements_txt(self):
35
- filename = 'requirements.txt'
36
- if not self._path_exists(filename):
37
- next_major_version = int(major_version) + 1
38
- content = f'squirrels<{next_major_version}'
39
- with open(filename, 'w') as f:
40
- f.write(content)
41
-
42
- def init_project(self, args):
43
- options = ['core', 'db_view', 'connections', 'context', 'selections_cfg', 'final_view', 'sample_db']
44
- answers = { x: getattr(args, x) for x in options }
45
- if not any(answers.values()):
46
- core_questions = [
47
- inquirer.Confirm('core',
48
- message="Include all core project files?",
49
- default=True)
50
- ]
51
- answers = inquirer.prompt(core_questions)
52
-
53
- if answers.get('core', False):
54
- conditional_questions = [
55
- inquirer.List('db_view',
56
- message="What's the file format for the database view?",
57
- choices=c.FILE_TYPE_CHOICES),
58
- ]
59
- answers.update(inquirer.prompt(conditional_questions))
60
-
61
- remaining_questions = [
62
- inquirer.Confirm('connections',
63
- message=f"Do you want to add a '{c.CONNECTIONS_FILE}' file?" ,
64
- default=False),
65
- inquirer.Confirm('context',
66
- message=f"Do you want to add a '{c.CONTEXT_FILE}' file?" ,
67
- default=False),
68
- inquirer.Confirm('selections_cfg',
69
- message=f"Do you want to add a '{c.SELECTIONS_CFG_FILE}' file?" ,
70
- default=False),
71
- inquirer.List('final_view',
72
- message="What's the file format for the final view (if any)?",
73
- choices=['none'] + c.FILE_TYPE_CHOICES),
74
- inquirer.List('sample_db',
75
- message="What sample sqlite database do you wish to use (if any)?",
76
- choices=['none'] + c.DATABASE_CHOICES)
77
- ]
78
- answers.update(inquirer.prompt(remaining_questions))
79
-
80
- if answers.get('core', False):
81
- self._copy_file('.gitignore')
82
- self._copy_file(c.MANIFEST_FILE)
83
- self._create_requirements_txt()
84
- self._copy_dataset_file(c.PARAMETERS_FILE)
85
- if answers.get('db_view') == 'py':
86
- self._copy_dataset_file(c.DATABASE_VIEW_PY_FILE)
87
- else:
88
- self._copy_dataset_file(c.DATABASE_VIEW_SQL_FILE)
89
-
90
- if answers.get('connections', False):
91
- self._copy_file(c.CONNECTIONS_FILE)
92
-
93
- if answers.get('context', False):
94
- self._copy_dataset_file(c.CONTEXT_FILE)
95
-
96
- if answers.get('selections_cfg', False):
97
- self._copy_dataset_file(c.SELECTIONS_CFG_FILE)
98
-
99
- final_view_format = answers.get('final_view')
100
- if final_view_format == 'py':
101
- self._copy_dataset_file(c.FINAL_VIEW_PY_NAME)
102
- elif final_view_format == 'sql':
103
- self._copy_dataset_file(c.FINAL_VIEW_SQL_NAME)
104
-
105
- sample_db = answers.get('sample_db')
106
- if sample_db == 'sample_database':
107
- self._copy_database_file('sample_database.db')
108
- elif sample_db == 'seattle_weather':
109
- self._copy_database_file('seattle_weather.db')
110
-
1
+ from typing import Optional
2
+ from datetime import datetime
3
+ from pathlib import Path
4
+ import inquirer, os, shutil, secrets
5
+
6
+ from . import _constants as c, _utils as u
7
+
8
+ base_proj_dir = Path(os.path.dirname(__file__), c.PACKAGE_DATA_FOLDER, c.BASE_PROJECT_FOLDER)
9
+
10
+ TMP_FOLDER = "tmp"
11
+
12
+
13
+ class Initializer:
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
17
+
18
+ def _path_exists(self, filepath: Path) -> bool:
19
+ return os.path.exists(filepath)
20
+
21
+ def _files_have_same_content(self, file1: Path, file2: Path) -> bool:
22
+ with open(file1, 'rb') as f1, open(file2, 'rb') as f2:
23
+ return f1.read() == f2.read()
24
+
25
+ def _add_timestamp_to_filename(self, path: Path) -> Path:
26
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
27
+ new_filename = f"{path.stem}_{timestamp}{path.suffix}"
28
+ return path.with_name(new_filename)
29
+
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)
33
+
34
+ filepath2 = Path(self.project_name, filepath) if self.project_name else filepath
35
+ dest_dir = os.path.dirname(filepath2)
36
+ if dest_dir != "":
37
+ os.makedirs(dest_dir, exist_ok=True)
38
+
39
+ perform_copy = True
40
+ if self._path_exists(filepath2):
41
+ old_filepath = filepath2
42
+ if self._files_have_same_content(src_path, filepath2):
43
+ perform_copy = False
44
+ extra_msg = "Skipping... file contents is same as source"
45
+ else:
46
+ filepath2 = self._add_timestamp_to_filename(old_filepath)
47
+ extra_msg = f'Creating file as "{filepath2}" instead...'
48
+ print(f'File "{old_filepath}" already exists.', extra_msg)
49
+ else:
50
+ print(f'Creating file "{filepath2}"...')
51
+
52
+ if perform_copy:
53
+ shutil.copy(src_path, filepath2)
54
+
55
+ def _copy_macros_file(self, filepath: str):
56
+ self._copy_file(Path(c.MACROS_FOLDER, filepath))
57
+
58
+ def _copy_models_file(self, filepath: str):
59
+ self._copy_file(Path(c.MODELS_FOLDER, filepath))
60
+
61
+ def _copy_build_file(self, filepath: str):
62
+ self._copy_file(Path(c.MODELS_FOLDER, c.BUILDS_FOLDER, filepath))
63
+
64
+ def _copy_dbview_file(self, filepath: str):
65
+ self._copy_file(Path(c.MODELS_FOLDER, c.DBVIEWS_FOLDER, filepath))
66
+
67
+ def _copy_federate_file(self, filepath: str):
68
+ self._copy_file(Path(c.MODELS_FOLDER, c.FEDERATES_FOLDER, filepath))
69
+
70
+ def _copy_resource_file(self, filepath: str):
71
+ self._copy_file(Path(c.RESOURCES_FOLDER, filepath))
72
+
73
+ def _copy_pyconfig_file(self, filepath: str):
74
+ self._copy_file(Path(c.PYCONFIGS_FOLDER, filepath))
75
+
76
+ def _copy_seed_file(self, filepath: str):
77
+ self._copy_file(Path(c.SEEDS_FOLDER, filepath))
78
+
79
+ def _copy_dashboard_file(self, filepath: str):
80
+ self._copy_file(Path(c.DASHBOARDS_FOLDER, filepath))
81
+
82
+ def _create_manifest_file(self, has_connections: bool, has_parameters: bool):
83
+ def get_content(file_name: Optional[str]) -> str:
84
+ if file_name is None:
85
+ return ""
86
+
87
+ yaml_path = Path(base_proj_dir, file_name)
88
+ return yaml_path.read_text()
89
+
90
+ file_name_dict = {
91
+ "parameters": c.PARAMETERS_YML_FILE if has_parameters else None,
92
+ "connections": c.CONNECTIONS_YML_FILE if has_connections else None,
93
+ }
94
+ substitutions = {key: get_content(val) for key, val in file_name_dict.items()}
95
+
96
+ manifest_template = get_content(c.MANIFEST_JINJA_FILE)
97
+ manifest_content = u.render_string(manifest_template, **substitutions)
98
+ output_path = Path(base_proj_dir, TMP_FOLDER, c.MANIFEST_FILE)
99
+ output_path.write_text(manifest_content)
100
+
101
+ self._copy_file(Path(c.MANIFEST_FILE), src_folder=TMP_FOLDER)
102
+
103
+ def _copy_dotenv_files(self, admin_password: str | None = None):
104
+ substitutions = {
105
+ "random_secret_key": secrets.token_hex(32),
106
+ "random_admin_password": admin_password if admin_password else secrets.token_urlsafe(8),
107
+ }
108
+
109
+ dotenv_path = Path(base_proj_dir, c.DOTENV_FILE)
110
+ contents = u.render_string(dotenv_path.read_text(), **substitutions)
111
+
112
+ output_path = Path(base_proj_dir, TMP_FOLDER, c.DOTENV_FILE)
113
+ output_path.write_text(contents)
114
+
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"))
120
+
121
+ def init_project(self, args):
122
+ options = ["connections", "parameters", "build", "federate", "dashboard", "admin_password"]
123
+ CONNECTIONS, PARAMETERS, BUILD, FEDERATE, DASHBOARD, ADMIN_PASSWORD = options
124
+
125
+ # Add project name prompt if not provided
126
+ if self.project_name is None and not self.use_curr_dir:
127
+ questions = [
128
+ inquirer.Text('project_name', message="What is your project folder name? (leave blank to create in current directory)")
129
+ ]
130
+ answers = inquirer.prompt(questions)
131
+ assert isinstance(answers, dict)
132
+ self.project_name = answers['project_name']
133
+
134
+ answers = { x: getattr(args, x) for x in options }
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)
179
+
180
+ def get_answer(key, default):
181
+ """
182
+ If key is in answers dict as None, using `.get` on a dictionary will return None even if a default is provided.
183
+
184
+ For instance, the following prints None.
185
+ >>> test_dict = {"key": None}
186
+ >>> print(test_dict.get("key", "default"))
187
+
188
+ This function will return the default value if the key is in the dict with value None.
189
+ """
190
+ answer = answers.get(key)
191
+ return answer if answer is not None else default
192
+
193
+ admin_password = get_answer("admin_password", None)
194
+
195
+ connections_format = get_answer(CONNECTIONS, c.YML_FORMAT)
196
+ connections_use_yaml = (connections_format == c.YML_FORMAT)
197
+ connections_use_py = (connections_format == c.PYTHON_FORMAT)
198
+
199
+ parameters_format = get_answer(PARAMETERS, c.PYTHON_FORMAT)
200
+ parameters_use_yaml = (parameters_format == c.YML_FORMAT)
201
+ parameters_use_py = (parameters_format == c.PYTHON_FORMAT)
202
+
203
+ build_config_file = c.BUILD_FILE_STEM + ".yml"
204
+ build_format = get_answer(BUILD, c.SQL_FILE_TYPE)
205
+ if build_format == c.SQL_FILE_TYPE:
206
+ build_file = c.BUILD_FILE_STEM + ".sql"
207
+ elif build_format == c.PYTHON_FILE_TYPE:
208
+ build_file = c.BUILD_FILE_STEM + ".py"
209
+ else:
210
+ raise NotImplementedError(f"Build model format '{build_format}' not supported")
211
+
212
+ db_view_config_file = c.DBVIEW_FILE_STEM + ".yml"
213
+ db_view_file = c.DBVIEW_FILE_STEM + ".sql"
214
+
215
+ federate_config_file = c.FEDERATE_FILE_STEM + ".yml"
216
+ federate_format = get_answer(FEDERATE, c.SQL_FILE_TYPE)
217
+ if federate_format == c.SQL_FILE_TYPE:
218
+ federate_file = c.FEDERATE_FILE_STEM + ".sql"
219
+ elif federate_format == c.PYTHON_FILE_TYPE:
220
+ federate_file = c.FEDERATE_FILE_STEM + ".py"
221
+ else:
222
+ raise NotImplementedError(f"Federate model format '{federate_format}' not supported")
223
+
224
+ dashboards_enabled = get_answer(DASHBOARD, False)
225
+
226
+ self._copy_dotenv_files(admin_password)
227
+ self._create_manifest_file(connections_use_yaml, parameters_use_yaml)
228
+
229
+ self._copy_gitignore_file()
230
+
231
+ if connections_use_py:
232
+ self._copy_pyconfig_file(c.CONNECTIONS_FILE)
233
+ elif connections_use_yaml:
234
+ pass # already included in squirrels.yml
235
+ else:
236
+ raise NotImplementedError(f"Format '{connections_format}' not supported for configuring database connections")
237
+
238
+ if parameters_use_py:
239
+ self._copy_pyconfig_file(c.PARAMETERS_FILE)
240
+ elif parameters_use_yaml:
241
+ pass # already included in squirrels.yml
242
+ else:
243
+ raise NotImplementedError(f"Format '{parameters_format}' not supported for configuring widget parameters")
244
+
245
+ self._copy_pyconfig_file(c.USER_FILE)
246
+
247
+ self._copy_pyconfig_file(c.CONTEXT_FILE)
248
+ self._copy_seed_file(c.SEED_CATEGORY_FILE_STEM + ".csv")
249
+ self._copy_seed_file(c.SEED_CATEGORY_FILE_STEM + ".yml")
250
+ self._copy_seed_file(c.SEED_SUBCATEGORY_FILE_STEM + ".csv")
251
+ self._copy_seed_file(c.SEED_SUBCATEGORY_FILE_STEM + ".yml")
252
+
253
+ self._copy_macros_file(c.MACROS_FILE)
254
+
255
+ self._copy_models_file(c.SOURCES_FILE)
256
+ self._copy_build_file(build_file)
257
+ self._copy_build_file(build_config_file)
258
+ self._copy_dbview_file(db_view_file)
259
+ self._copy_dbview_file(db_view_config_file)
260
+ self._copy_federate_file(federate_file)
261
+ self._copy_federate_file(federate_config_file)
262
+
263
+ if dashboards_enabled:
264
+ self._copy_dashboard_file(c.DASHBOARD_FILE_STEM + ".py")
265
+ self._copy_dashboard_file(c.DASHBOARD_FILE_STEM + ".yml")
266
+
267
+ self._copy_resource_file(c.EXPENSES_DB)
268
+ self._copy_file(Path(c.RESOURCES_FOLDER, c.PUBLIC_FOLDER, ".gitkeep"))
269
+
270
+ print(f"\nSuccessfully created new Squirrels project!\n")
271
+
272
+ def get_file(self, args):
273
+ if args.file_name == c.DOTENV_FILE:
274
+ self._copy_dotenv_files()
275
+ print(f"A random admin password was generated for your project. You can change it in the new {c.DOTENV_FILE} file.")
276
+ print()
277
+ print(f"IMPORTANT: Please ensure the {c.DOTENV_FILE} file is added to your {c.GITIGNORE_FILE} file.")
278
+ print(f"You may also run `sqrl get-file {c.GITIGNORE_FILE}` to add a sample {c.GITIGNORE_FILE} file to your project.")
279
+ print()
280
+ elif args.file_name == c.GITIGNORE_FILE:
281
+ self._copy_gitignore_file()
282
+ elif args.file_name == c.MANIFEST_FILE:
283
+ self._create_manifest_file(not args.no_connections, args.parameters)
284
+ elif args.file_name in (c.USER_FILE, c.CONNECTIONS_FILE, c.PARAMETERS_FILE, c.CONTEXT_FILE):
285
+ self._copy_pyconfig_file(args.file_name)
286
+ elif args.file_name == c.MACROS_FILE:
287
+ self._copy_macros_file(args.file_name)
288
+ elif args.file_name == c.SOURCES_FILE:
289
+ self._copy_models_file(args.file_name)
290
+ elif args.file_name in (c.BUILD_FILE_STEM, c.DBVIEW_FILE_STEM, c.FEDERATE_FILE_STEM):
291
+ if args.file_name == c.DBVIEW_FILE_STEM or args.format == c.SQL_FILE_TYPE:
292
+ extension = ".sql"
293
+ elif args.format == c.PYTHON_FILE_TYPE:
294
+ extension = ".py"
295
+ else:
296
+ raise NotImplementedError(f"Format '{args.format}' not supported for {args.file_name}")
297
+
298
+ if args.file_name == c.BUILD_FILE_STEM:
299
+ copy_method = self._copy_build_file
300
+ elif args.file_name == c.DBVIEW_FILE_STEM:
301
+ copy_method = self._copy_dbview_file
302
+ elif args.file_name == c.FEDERATE_FILE_STEM:
303
+ copy_method = self._copy_federate_file
304
+ else:
305
+ raise NotImplementedError(f"File '{args.file_name}' not supported")
306
+
307
+ copy_method(args.file_name + extension)
308
+ copy_method(args.file_name + ".yml")
309
+ elif args.file_name == c.DASHBOARD_FILE_STEM:
310
+ self._copy_dashboard_file(args.file_name + ".py")
311
+ self._copy_dashboard_file(args.file_name + ".yml")
312
+ elif args.file_name in (c.EXPENSES_DB, c.WEATHER_DB):
313
+ self._copy_resource_file(args.file_name)
314
+ elif args.file_name in (c.SEED_CATEGORY_FILE_STEM, c.SEED_SUBCATEGORY_FILE_STEM):
315
+ self._copy_seed_file(args.file_name + ".csv")
316
+ self._copy_seed_file(args.file_name + ".yml")
317
+ else:
318
+ raise NotImplementedError(f"File '{args.file_name}' not supported")
319
+
squirrels/_logging.py ADDED
@@ -0,0 +1,121 @@
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.replace("{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
+ project_path: str, log_to_file: bool | str, log_level: str, log_format: str, log_file_size_mb: float, 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
+ if isinstance(log_to_file, str):
99
+ log_file_path = Path(project_path, log_to_file, c.LOGS_FILE)
100
+ else:
101
+ log_file_path = Path(project_path, c.LOGS_FOLDER, c.LOGS_FILE)
102
+
103
+ log_file_path.parent.mkdir(parents=True, exist_ok=True)
104
+
105
+ # Rotating file handler
106
+ file_handler = RotatingFileHandler(
107
+ log_file_path,
108
+ maxBytes=int(log_file_size_mb * 1024 * 1024),
109
+ backupCount=log_file_backup_count
110
+ )
111
+ file_handler.setLevel(log_level.upper())
112
+ file_handler.setFormatter(file_formatter)
113
+ logger.addHandler(file_handler)
114
+
115
+ else:
116
+ stdout_handler = l.StreamHandler()
117
+ stdout_handler.setLevel(log_level.upper())
118
+ stdout_handler.setFormatter(stdout_formatter)
119
+ logger.addHandler(stdout_handler)
120
+
121
+ return logger