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.
- dateutils/__init__.py +6 -0
- dateutils/_enums.py +25 -0
- squirrels/dateutils.py → dateutils/_implementation.py +409 -380
- dateutils/types.py +6 -0
- squirrels/__init__.py +21 -18
- squirrels/_api_routes/__init__.py +5 -0
- squirrels/_api_routes/auth.py +337 -0
- squirrels/_api_routes/base.py +196 -0
- squirrels/_api_routes/dashboards.py +156 -0
- squirrels/_api_routes/data_management.py +148 -0
- squirrels/_api_routes/datasets.py +220 -0
- squirrels/_api_routes/project.py +289 -0
- squirrels/_api_server.py +552 -134
- squirrels/_arguments/__init__.py +0 -0
- squirrels/_arguments/init_time_args.py +83 -0
- squirrels/_arguments/run_time_args.py +111 -0
- squirrels/_auth.py +777 -0
- squirrels/_command_line.py +239 -107
- squirrels/_compile_prompts.py +147 -0
- squirrels/_connection_set.py +94 -0
- squirrels/_constants.py +141 -64
- squirrels/_dashboards.py +179 -0
- squirrels/_data_sources.py +570 -0
- squirrels/_dataset_types.py +91 -0
- squirrels/_env_vars.py +209 -0
- squirrels/_exceptions.py +29 -0
- squirrels/_http_error_responses.py +52 -0
- squirrels/_initializer.py +319 -110
- squirrels/_logging.py +121 -0
- squirrels/_manifest.py +357 -187
- squirrels/_mcp_server.py +578 -0
- squirrels/_model_builder.py +69 -0
- squirrels/_model_configs.py +74 -0
- squirrels/_model_queries.py +52 -0
- squirrels/_models.py +1201 -0
- squirrels/_package_data/base_project/.env +7 -0
- squirrels/_package_data/base_project/.env.example +44 -0
- squirrels/_package_data/base_project/connections.yml +16 -0
- squirrels/_package_data/base_project/dashboards/dashboard_example.py +40 -0
- squirrels/_package_data/base_project/dashboards/dashboard_example.yml +22 -0
- squirrels/_package_data/base_project/docker/.dockerignore +16 -0
- squirrels/_package_data/base_project/docker/Dockerfile +16 -0
- squirrels/_package_data/base_project/docker/compose.yml +7 -0
- squirrels/_package_data/base_project/duckdb_init.sql +10 -0
- squirrels/_package_data/base_project/gitignore +13 -0
- squirrels/_package_data/base_project/macros/macros_example.sql +17 -0
- squirrels/_package_data/base_project/models/builds/build_example.py +26 -0
- squirrels/_package_data/base_project/models/builds/build_example.sql +16 -0
- squirrels/_package_data/base_project/models/builds/build_example.yml +57 -0
- squirrels/_package_data/base_project/models/dbviews/dbview_example.sql +17 -0
- squirrels/_package_data/base_project/models/dbviews/dbview_example.yml +32 -0
- squirrels/_package_data/base_project/models/federates/federate_example.py +51 -0
- squirrels/_package_data/base_project/models/federates/federate_example.sql +21 -0
- squirrels/_package_data/base_project/models/federates/federate_example.yml +65 -0
- squirrels/_package_data/base_project/models/sources.yml +38 -0
- squirrels/_package_data/base_project/parameters.yml +142 -0
- squirrels/_package_data/base_project/pyconfigs/connections.py +19 -0
- squirrels/_package_data/base_project/pyconfigs/context.py +96 -0
- squirrels/_package_data/base_project/pyconfigs/parameters.py +141 -0
- squirrels/_package_data/base_project/pyconfigs/user.py +56 -0
- squirrels/_package_data/base_project/resources/expenses.db +0 -0
- squirrels/_package_data/base_project/resources/public/.gitkeep +0 -0
- squirrels/_package_data/base_project/resources/weather.db +0 -0
- squirrels/_package_data/base_project/seeds/seed_categories.csv +6 -0
- squirrels/_package_data/base_project/seeds/seed_categories.yml +15 -0
- squirrels/_package_data/base_project/seeds/seed_subcategories.csv +15 -0
- squirrels/_package_data/base_project/seeds/seed_subcategories.yml +21 -0
- squirrels/_package_data/base_project/squirrels.yml.j2 +61 -0
- squirrels/_package_data/base_project/tmp/.gitignore +2 -0
- squirrels/_package_data/templates/login_successful.html +53 -0
- squirrels/_package_data/templates/squirrels_studio.html +22 -0
- squirrels/_package_loader.py +29 -0
- squirrels/_parameter_configs.py +592 -0
- squirrels/_parameter_options.py +348 -0
- squirrels/_parameter_sets.py +207 -0
- squirrels/_parameters.py +1703 -0
- squirrels/_project.py +796 -0
- squirrels/_py_module.py +122 -0
- squirrels/_request_context.py +33 -0
- squirrels/_schemas/__init__.py +0 -0
- squirrels/_schemas/auth_models.py +83 -0
- squirrels/_schemas/query_param_models.py +70 -0
- squirrels/_schemas/request_models.py +26 -0
- squirrels/_schemas/response_models.py +286 -0
- squirrels/_seeds.py +97 -0
- squirrels/_sources.py +112 -0
- squirrels/_utils.py +540 -149
- squirrels/_version.py +1 -3
- squirrels/arguments.py +7 -0
- squirrels/auth.py +4 -0
- squirrels/connections.py +3 -0
- squirrels/dashboards.py +3 -0
- squirrels/data_sources.py +14 -282
- squirrels/parameter_options.py +13 -189
- squirrels/parameters.py +14 -801
- squirrels/types.py +18 -0
- squirrels-0.6.0.post0.dist-info/METADATA +148 -0
- squirrels-0.6.0.post0.dist-info/RECORD +101 -0
- {squirrels-0.1.0.dist-info → squirrels-0.6.0.post0.dist-info}/WHEEL +1 -2
- {squirrels-0.1.0.dist-info → squirrels-0.6.0.post0.dist-info}/entry_points.txt +1 -0
- squirrels-0.6.0.post0.dist-info/licenses/LICENSE +201 -0
- squirrels/_credentials_manager.py +0 -87
- squirrels/_module_loader.py +0 -37
- squirrels/_parameter_set.py +0 -151
- squirrels/_renderer.py +0 -286
- squirrels/_timed_imports.py +0 -37
- squirrels/connection_set.py +0 -126
- squirrels/package_data/base_project/.gitignore +0 -4
- squirrels/package_data/base_project/connections.py +0 -21
- squirrels/package_data/base_project/database/sample_database.db +0 -0
- squirrels/package_data/base_project/database/seattle_weather.db +0 -0
- squirrels/package_data/base_project/datasets/sample_dataset/context.py +0 -8
- squirrels/package_data/base_project/datasets/sample_dataset/database_view1.py +0 -23
- squirrels/package_data/base_project/datasets/sample_dataset/database_view1.sql.j2 +0 -7
- squirrels/package_data/base_project/datasets/sample_dataset/final_view.py +0 -10
- squirrels/package_data/base_project/datasets/sample_dataset/final_view.sql.j2 +0 -2
- squirrels/package_data/base_project/datasets/sample_dataset/parameters.py +0 -30
- squirrels/package_data/base_project/datasets/sample_dataset/selections.cfg +0 -6
- squirrels/package_data/base_project/squirrels.yaml +0 -26
- squirrels/package_data/static/favicon.ico +0 -0
- squirrels/package_data/static/script.js +0 -234
- squirrels/package_data/static/style.css +0 -110
- squirrels/package_data/templates/index.html +0 -32
- squirrels-0.1.0.dist-info/LICENSE +0 -22
- squirrels-0.1.0.dist-info/METADATA +0 -67
- squirrels-0.1.0.dist-info/RECORD +0 -40
- squirrels-0.1.0.dist-info/top_level.txt +0 -1
squirrels/_initializer.py
CHANGED
|
@@ -1,110 +1,319 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
from
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def
|
|
15
|
-
if not
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|