squirrels 0.4.1__py3-none-any.whl → 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of squirrels might be problematic. Click here for more details.
- dateutils/__init__.py +6 -0
- dateutils/_enums.py +25 -0
- squirrels/dateutils.py → dateutils/_implementation.py +58 -111
- dateutils/types.py +6 -0
- squirrels/__init__.py +13 -11
- squirrels/_api_routes/__init__.py +5 -0
- squirrels/_api_routes/auth.py +271 -0
- squirrels/_api_routes/base.py +165 -0
- squirrels/_api_routes/dashboards.py +150 -0
- squirrels/_api_routes/data_management.py +145 -0
- squirrels/_api_routes/datasets.py +257 -0
- squirrels/_api_routes/oauth2.py +298 -0
- squirrels/_api_routes/project.py +252 -0
- squirrels/_api_server.py +256 -450
- squirrels/_arguments/__init__.py +0 -0
- squirrels/_arguments/init_time_args.py +108 -0
- squirrels/_arguments/run_time_args.py +147 -0
- squirrels/_auth.py +960 -0
- squirrels/_command_line.py +126 -45
- squirrels/_compile_prompts.py +147 -0
- squirrels/_connection_set.py +48 -26
- squirrels/_constants.py +68 -38
- squirrels/_dashboards.py +160 -0
- squirrels/_data_sources.py +570 -0
- squirrels/_dataset_types.py +84 -0
- squirrels/_exceptions.py +29 -0
- squirrels/_initializer.py +177 -80
- squirrels/_logging.py +115 -0
- squirrels/_manifest.py +208 -79
- squirrels/_model_builder.py +69 -0
- squirrels/_model_configs.py +74 -0
- squirrels/_model_queries.py +52 -0
- squirrels/_models.py +926 -367
- squirrels/_package_data/base_project/.env +42 -0
- squirrels/_package_data/base_project/.env.example +42 -0
- squirrels/_package_data/base_project/assets/expenses.db +0 -0
- squirrels/_package_data/base_project/connections.yml +16 -0
- squirrels/_package_data/base_project/dashboards/dashboard_example.py +34 -0
- squirrels/_package_data/base_project/dashboards/dashboard_example.yml +22 -0
- squirrels/{package_data → _package_data}/base_project/docker/.dockerignore +5 -2
- squirrels/{package_data → _package_data}/base_project/docker/Dockerfile +3 -3
- squirrels/{package_data → _package_data}/base_project/docker/compose.yml +1 -1
- squirrels/_package_data/base_project/duckdb_init.sql +10 -0
- squirrels/{package_data/base_project/.gitignore → _package_data/base_project/gitignore} +3 -2
- 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 +12 -0
- squirrels/_package_data/base_project/models/dbviews/dbview_example.yml +26 -0
- squirrels/_package_data/base_project/models/federates/federate_example.py +37 -0
- squirrels/_package_data/base_project/models/federates/federate_example.sql +19 -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 → _package_data}/base_project/parameters.yml +56 -40
- squirrels/_package_data/base_project/pyconfigs/connections.py +14 -0
- squirrels/{package_data → _package_data}/base_project/pyconfigs/context.py +21 -40
- squirrels/_package_data/base_project/pyconfigs/parameters.py +141 -0
- squirrels/_package_data/base_project/pyconfigs/user.py +44 -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/templates/dataset_results.html +112 -0
- squirrels/_package_data/templates/oauth_login.html +271 -0
- squirrels/_package_data/templates/squirrels_studio.html +20 -0
- squirrels/_package_loader.py +8 -4
- squirrels/_parameter_configs.py +104 -103
- squirrels/_parameter_options.py +348 -0
- squirrels/_parameter_sets.py +57 -47
- squirrels/_parameters.py +1664 -0
- squirrels/_project.py +721 -0
- squirrels/_py_module.py +7 -5
- squirrels/_schemas/__init__.py +0 -0
- squirrels/_schemas/auth_models.py +167 -0
- squirrels/_schemas/query_param_models.py +75 -0
- squirrels/{_api_response_models.py → _schemas/response_models.py} +126 -47
- squirrels/_seeds.py +35 -16
- squirrels/_sources.py +110 -0
- squirrels/_utils.py +248 -73
- squirrels/_version.py +1 -1
- squirrels/arguments.py +7 -0
- squirrels/auth.py +4 -0
- squirrels/connections.py +3 -0
- squirrels/dashboards.py +2 -81
- squirrels/data_sources.py +14 -631
- squirrels/parameter_options.py +13 -348
- squirrels/parameters.py +14 -1266
- squirrels/types.py +16 -0
- squirrels-0.5.0.dist-info/METADATA +113 -0
- squirrels-0.5.0.dist-info/RECORD +97 -0
- {squirrels-0.4.1.dist-info → squirrels-0.5.0.dist-info}/WHEEL +1 -1
- squirrels-0.5.0.dist-info/entry_points.txt +3 -0
- {squirrels-0.4.1.dist-info → squirrels-0.5.0.dist-info/licenses}/LICENSE +1 -1
- squirrels/_authenticator.py +0 -85
- squirrels/_dashboards_io.py +0 -61
- squirrels/_environcfg.py +0 -84
- squirrels/arguments/init_time_args.py +0 -40
- squirrels/arguments/run_time_args.py +0 -208
- squirrels/package_data/assets/favicon.ico +0 -0
- squirrels/package_data/assets/index.css +0 -1
- squirrels/package_data/assets/index.js +0 -58
- squirrels/package_data/base_project/assets/expenses.db +0 -0
- squirrels/package_data/base_project/connections.yml +0 -7
- squirrels/package_data/base_project/dashboards/dashboard_example.py +0 -32
- squirrels/package_data/base_project/dashboards.yml +0 -10
- squirrels/package_data/base_project/env.yml +0 -29
- squirrels/package_data/base_project/models/dbviews/dbview_example.py +0 -47
- squirrels/package_data/base_project/models/dbviews/dbview_example.sql +0 -22
- squirrels/package_data/base_project/models/federates/federate_example.py +0 -21
- squirrels/package_data/base_project/models/federates/federate_example.sql +0 -3
- squirrels/package_data/base_project/pyconfigs/auth.py +0 -45
- squirrels/package_data/base_project/pyconfigs/connections.py +0 -19
- squirrels/package_data/base_project/pyconfigs/parameters.py +0 -95
- squirrels/package_data/base_project/seeds/seed_subcategories.csv +0 -15
- squirrels/package_data/base_project/squirrels.yml.j2 +0 -94
- squirrels/package_data/templates/index.html +0 -18
- squirrels/project.py +0 -378
- squirrels/user_base.py +0 -55
- squirrels-0.4.1.dist-info/METADATA +0 -117
- squirrels-0.4.1.dist-info/RECORD +0 -60
- squirrels-0.4.1.dist-info/entry_points.txt +0 -4
- /squirrels/{package_data → _package_data}/base_project/assets/weather.db +0 -0
- /squirrels/{package_data → _package_data}/base_project/seeds/seed_categories.csv +0 -0
- /squirrels/{package_data → _package_data}/base_project/tmp/.gitignore +0 -0
squirrels/_initializer.py
CHANGED
|
@@ -1,124 +1,181 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
2
|
from datetime import datetime
|
|
3
|
-
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import inquirer, os, shutil, secrets
|
|
4
5
|
|
|
5
6
|
from . import _constants as c, _utils as u
|
|
6
7
|
|
|
7
|
-
base_proj_dir =
|
|
8
|
+
base_proj_dir = Path(os.path.dirname(__file__), c.PACKAGE_DATA_FOLDER, c.BASE_PROJECT_FOLDER)
|
|
9
|
+
|
|
10
|
+
TMP_FOLDER = "tmp"
|
|
8
11
|
|
|
9
12
|
|
|
10
13
|
class Initializer:
|
|
11
|
-
def __init__(self, *,
|
|
12
|
-
self.
|
|
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
|
|
13
17
|
|
|
14
|
-
def _path_exists(self, filepath:
|
|
18
|
+
def _path_exists(self, filepath: Path) -> bool:
|
|
15
19
|
return os.path.exists(filepath)
|
|
16
20
|
|
|
17
|
-
def _files_have_same_content(self, file1:
|
|
21
|
+
def _files_have_same_content(self, file1: Path, file2: Path) -> bool:
|
|
18
22
|
with open(file1, 'rb') as f1, open(file2, 'rb') as f2:
|
|
19
23
|
return f1.read() == f2.read()
|
|
20
24
|
|
|
21
|
-
def _add_timestamp_to_filename(self, path:
|
|
25
|
+
def _add_timestamp_to_filename(self, path: Path) -> Path:
|
|
22
26
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
23
27
|
new_filename = f"{path.stem}_{timestamp}{path.suffix}"
|
|
24
28
|
return path.with_name(new_filename)
|
|
25
29
|
|
|
26
|
-
def _copy_file(self, filepath:
|
|
27
|
-
|
|
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)
|
|
28
33
|
|
|
29
|
-
|
|
34
|
+
filepath2 = Path(self.project_name, filepath) if self.project_name else filepath
|
|
35
|
+
dest_dir = os.path.dirname(filepath2)
|
|
30
36
|
if dest_dir != "":
|
|
31
37
|
os.makedirs(dest_dir, exist_ok=True)
|
|
32
38
|
|
|
33
39
|
perform_copy = True
|
|
34
|
-
if self._path_exists(
|
|
35
|
-
old_filepath =
|
|
36
|
-
if self._files_have_same_content(src_path,
|
|
40
|
+
if self._path_exists(filepath2):
|
|
41
|
+
old_filepath = filepath2
|
|
42
|
+
if self._files_have_same_content(src_path, filepath2):
|
|
37
43
|
perform_copy = False
|
|
38
44
|
extra_msg = "Skipping... file contents is same as source"
|
|
39
|
-
elif self.overwrite:
|
|
40
|
-
extra_msg = "Overwriting file..."
|
|
41
45
|
else:
|
|
42
|
-
|
|
43
|
-
extra_msg = f'Creating file as "{
|
|
46
|
+
filepath2 = self._add_timestamp_to_filename(old_filepath)
|
|
47
|
+
extra_msg = f'Creating file as "{filepath2}" instead...'
|
|
44
48
|
print(f'File "{old_filepath}" already exists.', extra_msg)
|
|
45
49
|
else:
|
|
46
|
-
print(f'Creating file "{
|
|
50
|
+
print(f'Creating file "{filepath2}"...')
|
|
47
51
|
|
|
48
52
|
if perform_copy:
|
|
49
|
-
shutil.copy(src_path,
|
|
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))
|
|
50
63
|
|
|
51
64
|
def _copy_dbview_file(self, filepath: str):
|
|
52
|
-
self._copy_file(
|
|
65
|
+
self._copy_file(Path(c.MODELS_FOLDER, c.DBVIEWS_FOLDER, filepath))
|
|
53
66
|
|
|
54
67
|
def _copy_federate_file(self, filepath: str):
|
|
55
|
-
self._copy_file(
|
|
68
|
+
self._copy_file(Path(c.MODELS_FOLDER, c.FEDERATES_FOLDER, filepath))
|
|
56
69
|
|
|
57
70
|
def _copy_database_file(self, filepath: str):
|
|
58
|
-
self._copy_file(
|
|
71
|
+
self._copy_file(Path(c.DATABASE_FOLDER, filepath))
|
|
59
72
|
|
|
60
73
|
def _copy_pyconfig_file(self, filepath: str):
|
|
61
|
-
self._copy_file(
|
|
74
|
+
self._copy_file(Path(c.PYCONFIGS_FOLDER, filepath))
|
|
62
75
|
|
|
63
76
|
def _copy_seed_file(self, filepath: str):
|
|
64
|
-
self._copy_file(
|
|
77
|
+
self._copy_file(Path(c.SEEDS_FOLDER, filepath))
|
|
65
78
|
|
|
66
79
|
def _copy_dashboard_file(self, filepath: str):
|
|
67
|
-
self._copy_file(
|
|
68
|
-
|
|
69
|
-
def _create_manifest_file(self, has_connections: bool, has_parameters: bool, has_dashboards: bool):
|
|
70
|
-
TMP_FOLDER = "tmp"
|
|
80
|
+
self._copy_file(Path(c.DASHBOARDS_FOLDER, filepath))
|
|
71
81
|
|
|
82
|
+
def _create_manifest_file(self, has_connections: bool, has_parameters: bool):
|
|
72
83
|
def get_content(file_name: Optional[str]) -> str:
|
|
73
84
|
if file_name is None:
|
|
74
85
|
return ""
|
|
75
86
|
|
|
76
|
-
yaml_path =
|
|
77
|
-
return
|
|
87
|
+
yaml_path = Path(base_proj_dir, file_name)
|
|
88
|
+
return yaml_path.read_text()
|
|
78
89
|
|
|
79
90
|
file_name_dict = {
|
|
80
91
|
"parameters": c.PARAMETERS_YML_FILE if has_parameters else None,
|
|
81
92
|
"connections": c.CONNECTIONS_YML_FILE if has_connections else None,
|
|
82
|
-
"dashboards": c.DASHBOARDS_YML_FILE if has_dashboards else None
|
|
83
93
|
}
|
|
84
94
|
substitutions = {key: get_content(val) for key, val in file_name_dict.items()}
|
|
85
95
|
|
|
86
96
|
manifest_template = get_content(c.MANIFEST_JINJA_FILE)
|
|
87
97
|
manifest_content = u.render_string(manifest_template, **substitutions)
|
|
88
|
-
output_path =
|
|
89
|
-
|
|
90
|
-
f.write(manifest_content)
|
|
98
|
+
output_path = Path(base_proj_dir, TMP_FOLDER, c.MANIFEST_FILE)
|
|
99
|
+
output_path.write_text(manifest_content)
|
|
91
100
|
|
|
92
|
-
self._copy_file(
|
|
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"))
|
|
93
120
|
|
|
94
121
|
def init_project(self, args):
|
|
95
|
-
options = ["
|
|
96
|
-
|
|
122
|
+
options = ["connections", "parameters", "build", "federate", "dashboard", "admin_password"]
|
|
123
|
+
CONNECTIONS, PARAMETERS, BUILD, FEDERATE, DASHBOARD, ADMIN_PASSWORD = options
|
|
97
124
|
|
|
98
|
-
|
|
99
|
-
if not
|
|
125
|
+
# Add project name prompt if not provided
|
|
126
|
+
if self.project_name is None and not args.curr_dir:
|
|
100
127
|
questions = [
|
|
101
|
-
inquirer.
|
|
102
|
-
CONNECTIONS, message=f"How would you like to configure the database connections?", choices=c.CONF_FORMAT_CHOICES
|
|
103
|
-
),
|
|
104
|
-
inquirer.List(
|
|
105
|
-
PARAMETERS, message=f"How would you like to configure the parameters?", choices=c.CONF_FORMAT_CHOICES2
|
|
106
|
-
),
|
|
107
|
-
inquirer.List(
|
|
108
|
-
DBVIEW, message="What's the file format for the database view model?", choices=c.FILE_TYPE_CHOICES
|
|
109
|
-
),
|
|
110
|
-
inquirer.List(
|
|
111
|
-
FEDERATE, message="What's the file format for the federated model?", choices=c.FILE_TYPE_CHOICES
|
|
112
|
-
),
|
|
113
|
-
inquirer.Confirm(
|
|
114
|
-
DASHBOARD, message=f"Do you want to include a dashboard example?", default=False
|
|
115
|
-
),
|
|
116
|
-
inquirer.Confirm(
|
|
117
|
-
AUTH, message=f"Do you want to add the '{c.AUTH_FILE}' file to enable custom API authentication?", default=False
|
|
118
|
-
),
|
|
128
|
+
inquirer.Text('project_name', message="What is your project folder name? (leave blank to create in current directory)")
|
|
119
129
|
]
|
|
120
130
|
answers = inquirer.prompt(questions)
|
|
121
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)
|
|
122
179
|
|
|
123
180
|
def get_answer(key, default):
|
|
124
181
|
"""
|
|
@@ -133,6 +190,8 @@ class Initializer:
|
|
|
133
190
|
answer = answers.get(key)
|
|
134
191
|
return answer if answer is not None else default
|
|
135
192
|
|
|
193
|
+
admin_password = get_answer("admin_password", None)
|
|
194
|
+
|
|
136
195
|
connections_format = get_answer(CONNECTIONS, c.YML_FORMAT)
|
|
137
196
|
connections_use_yaml = (connections_format == c.YML_FORMAT)
|
|
138
197
|
connections_use_py = (connections_format == c.PYTHON_FORMAT)
|
|
@@ -141,14 +200,19 @@ class Initializer:
|
|
|
141
200
|
parameters_use_yaml = (parameters_format == c.YML_FORMAT)
|
|
142
201
|
parameters_use_py = (parameters_format == c.PYTHON_FORMAT)
|
|
143
202
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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"
|
|
149
209
|
else:
|
|
150
|
-
raise NotImplementedError(f"
|
|
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"
|
|
151
214
|
|
|
215
|
+
federate_config_file = c.FEDERATE_FILE_STEM + ".yml"
|
|
152
216
|
federate_format = get_answer(FEDERATE, c.SQL_FILE_TYPE)
|
|
153
217
|
if federate_format == c.SQL_FILE_TYPE:
|
|
154
218
|
federate_file = c.FEDERATE_FILE_STEM + ".sql"
|
|
@@ -159,10 +223,10 @@ class Initializer:
|
|
|
159
223
|
|
|
160
224
|
dashboards_enabled = get_answer(DASHBOARD, False)
|
|
161
225
|
|
|
162
|
-
self.
|
|
226
|
+
self._copy_dotenv_files(admin_password)
|
|
227
|
+
self._create_manifest_file(connections_use_yaml, parameters_use_yaml)
|
|
163
228
|
|
|
164
|
-
self.
|
|
165
|
-
self._copy_file(u.Path(c.ENV_CONFIG_FILE))
|
|
229
|
+
self._copy_gitignore_file()
|
|
166
230
|
|
|
167
231
|
if connections_use_py:
|
|
168
232
|
self._copy_pyconfig_file(c.CONNECTIONS_FILE)
|
|
@@ -178,44 +242,77 @@ class Initializer:
|
|
|
178
242
|
else:
|
|
179
243
|
raise NotImplementedError(f"Format '{parameters_format}' not supported for configuring widget parameters")
|
|
180
244
|
|
|
245
|
+
self._copy_pyconfig_file(c.USER_FILE)
|
|
246
|
+
|
|
181
247
|
self._copy_pyconfig_file(c.CONTEXT_FILE)
|
|
182
|
-
self._copy_seed_file(c.
|
|
183
|
-
self._copy_seed_file(c.
|
|
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")
|
|
184
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)
|
|
185
258
|
self._copy_dbview_file(db_view_file)
|
|
259
|
+
self._copy_dbview_file(db_view_config_file)
|
|
186
260
|
self._copy_federate_file(federate_file)
|
|
261
|
+
self._copy_federate_file(federate_config_file)
|
|
187
262
|
|
|
188
263
|
if dashboards_enabled:
|
|
189
264
|
self._copy_dashboard_file(c.DASHBOARD_FILE_STEM + ".py")
|
|
265
|
+
self._copy_dashboard_file(c.DASHBOARD_FILE_STEM + ".yml")
|
|
190
266
|
|
|
191
|
-
if get_answer(AUTH, False):
|
|
192
|
-
self._copy_pyconfig_file(c.AUTH_FILE)
|
|
193
|
-
|
|
194
267
|
self._copy_database_file(c.EXPENSES_DB)
|
|
195
268
|
|
|
196
|
-
print(f"\nSuccessfully created new Squirrels project
|
|
269
|
+
print(f"\nSuccessfully created new Squirrels project!\n")
|
|
197
270
|
|
|
198
271
|
def get_file(self, args):
|
|
199
|
-
if args.file_name == c.
|
|
200
|
-
self.
|
|
201
|
-
print("
|
|
272
|
+
if args.file_name == c.DOTENV_FILE:
|
|
273
|
+
self._copy_dotenv_files()
|
|
274
|
+
print(f"A random admin password was generated for your project. You can change it in the new {c.DOTENV_FILE} file.")
|
|
275
|
+
print()
|
|
276
|
+
print(f"IMPORTANT: Please ensure the {c.DOTENV_FILE} file is added to your {c.GITIGNORE_FILE} file.")
|
|
277
|
+
print(f"You may also run `sqrl get-file {c.GITIGNORE_FILE}` to add a sample {c.GITIGNORE_FILE} file to your project.")
|
|
278
|
+
print()
|
|
279
|
+
elif args.file_name == c.GITIGNORE_FILE:
|
|
280
|
+
self._copy_gitignore_file()
|
|
202
281
|
elif args.file_name == c.MANIFEST_FILE:
|
|
203
|
-
self._create_manifest_file(not args.no_connections, args.parameters
|
|
204
|
-
elif args.file_name in (c.
|
|
282
|
+
self._create_manifest_file(not args.no_connections, args.parameters)
|
|
283
|
+
elif args.file_name in (c.USER_FILE, c.CONNECTIONS_FILE, c.PARAMETERS_FILE, c.CONTEXT_FILE):
|
|
205
284
|
self._copy_pyconfig_file(args.file_name)
|
|
206
|
-
elif args.file_name
|
|
207
|
-
|
|
285
|
+
elif args.file_name == c.MACROS_FILE:
|
|
286
|
+
self._copy_macros_file(args.file_name)
|
|
287
|
+
elif args.file_name == c.SOURCES_FILE:
|
|
288
|
+
self._copy_models_file(args.file_name)
|
|
289
|
+
elif args.file_name in (c.BUILD_FILE_STEM, c.DBVIEW_FILE_STEM, c.FEDERATE_FILE_STEM):
|
|
290
|
+
if args.file_name == c.DBVIEW_FILE_STEM or args.format == c.SQL_FILE_TYPE:
|
|
208
291
|
extension = ".sql"
|
|
209
292
|
elif args.format == c.PYTHON_FILE_TYPE:
|
|
210
293
|
extension = ".py"
|
|
211
294
|
else:
|
|
212
295
|
raise NotImplementedError(f"Format '{args.format}' not supported for {args.file_name}")
|
|
213
|
-
|
|
296
|
+
|
|
297
|
+
if args.file_name == c.BUILD_FILE_STEM:
|
|
298
|
+
copy_method = self._copy_build_file
|
|
299
|
+
elif args.file_name == c.DBVIEW_FILE_STEM:
|
|
300
|
+
copy_method = self._copy_dbview_file
|
|
301
|
+
elif args.file_name == c.FEDERATE_FILE_STEM:
|
|
302
|
+
copy_method = self._copy_federate_file
|
|
303
|
+
else:
|
|
304
|
+
raise NotImplementedError(f"File '{args.file_name}' not supported")
|
|
305
|
+
|
|
214
306
|
copy_method(args.file_name + extension)
|
|
307
|
+
copy_method(args.file_name + ".yml")
|
|
215
308
|
elif args.file_name == c.DASHBOARD_FILE_STEM:
|
|
216
309
|
self._copy_dashboard_file(args.file_name + ".py")
|
|
310
|
+
self._copy_dashboard_file(args.file_name + ".yml")
|
|
217
311
|
elif args.file_name in (c.EXPENSES_DB, c.WEATHER_DB):
|
|
218
312
|
self._copy_database_file(args.file_name)
|
|
313
|
+
elif args.file_name in (c.SEED_CATEGORY_FILE_STEM, c.SEED_SUBCATEGORY_FILE_STEM):
|
|
314
|
+
self._copy_seed_file(args.file_name + ".csv")
|
|
315
|
+
self._copy_seed_file(args.file_name + ".yml")
|
|
219
316
|
else:
|
|
220
317
|
raise NotImplementedError(f"File '{args.file_name}' not supported")
|
|
221
318
|
|
squirrels/_logging.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
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
|
+
|
|
8
|
+
|
|
9
|
+
class _BaseFormatter(l.Formatter):
|
|
10
|
+
def _format_helper(self, level_for_print: str, record: l.LogRecord) -> str:
|
|
11
|
+
# Save original levelname
|
|
12
|
+
original_levelname = record.levelname
|
|
13
|
+
|
|
14
|
+
# Add padding to the levelname for printing
|
|
15
|
+
visible_length = len(record.levelname) + 1
|
|
16
|
+
padding_needed = max(1, 9 - visible_length)
|
|
17
|
+
padded_level = f"{level_for_print}:{' ' * padding_needed}"
|
|
18
|
+
record.levelname = padded_level
|
|
19
|
+
|
|
20
|
+
# Format the message
|
|
21
|
+
formatted = super().format(record)
|
|
22
|
+
|
|
23
|
+
# Restore original levelname
|
|
24
|
+
record.levelname = original_levelname
|
|
25
|
+
|
|
26
|
+
return formatted
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _ColoredFormatter(_BaseFormatter):
|
|
30
|
+
"""Custom formatter that adds colors to log levels for terminal output"""
|
|
31
|
+
|
|
32
|
+
# ANSI color codes
|
|
33
|
+
COLORS = {
|
|
34
|
+
'DEBUG': '\033[36m', # Cyan
|
|
35
|
+
'INFO': '\033[32m', # Green
|
|
36
|
+
'WARNING': '\033[33m', # Yellow
|
|
37
|
+
'ERROR': '\033[31m', # Red
|
|
38
|
+
'CRITICAL': '\033[35m', # Magenta
|
|
39
|
+
}
|
|
40
|
+
RESET = '\033[0m'
|
|
41
|
+
BOLD = '\033[1m'
|
|
42
|
+
|
|
43
|
+
def format(self, record: l.LogRecord) -> str:
|
|
44
|
+
# Add color to levelname with colon and padding
|
|
45
|
+
color = self.COLORS.get(record.levelname, '')
|
|
46
|
+
colored_level = f"{color}{record.levelname}{self.RESET}"
|
|
47
|
+
return self._format_helper(colored_level, record)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class _PlainFormatter(_BaseFormatter):
|
|
51
|
+
"""Custom formatter that adds colon to log levels for file output"""
|
|
52
|
+
|
|
53
|
+
def format(self, record: l.LogRecord) -> str:
|
|
54
|
+
return self._format_helper(record.levelname, record)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class _CustomJsonFormatter(l.Formatter):
|
|
58
|
+
def format(self, record: l.LogRecord) -> str:
|
|
59
|
+
super().format(record)
|
|
60
|
+
info = {
|
|
61
|
+
"timestamp": self.formatTime(record),
|
|
62
|
+
"project_id": record.name,
|
|
63
|
+
"level": record.levelname,
|
|
64
|
+
"message": record.getMessage(),
|
|
65
|
+
"thread": record.thread,
|
|
66
|
+
"thread_name": record.threadName,
|
|
67
|
+
"process": record.process,
|
|
68
|
+
**record.__dict__.get("info", {})
|
|
69
|
+
}
|
|
70
|
+
output = {
|
|
71
|
+
"data": record.__dict__.get("data", {}),
|
|
72
|
+
"info": info
|
|
73
|
+
}
|
|
74
|
+
return json.dumps(output)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_logger(
|
|
78
|
+
base_path: str, log_to_file: bool, log_level: str, log_format: str, log_file_size_mb: int, log_file_backup_count: int
|
|
79
|
+
) -> u.Logger:
|
|
80
|
+
logger = u.Logger(name=uuid4().hex)
|
|
81
|
+
logger.setLevel(log_level.upper())
|
|
82
|
+
|
|
83
|
+
# Determine the formatter based on log_format
|
|
84
|
+
if log_format.lower() == "json":
|
|
85
|
+
stdout_formatter = _CustomJsonFormatter()
|
|
86
|
+
file_formatter = _CustomJsonFormatter()
|
|
87
|
+
elif log_format.lower() == "text":
|
|
88
|
+
# Use colored formatter for stdout, plain formatter with colon for file
|
|
89
|
+
format_string = "%(levelname)s [%(asctime)s] %(message)s"
|
|
90
|
+
stdout_formatter = _ColoredFormatter(format_string, datefmt="%Y-%m-%d %H:%M:%S")
|
|
91
|
+
file_formatter = _PlainFormatter(format_string, datefmt="%Y-%m-%d %H:%M:%S")
|
|
92
|
+
else:
|
|
93
|
+
raise ValueError("log_format must be either 'text' or 'json'")
|
|
94
|
+
|
|
95
|
+
if log_to_file:
|
|
96
|
+
log_file_path = Path(base_path, c.LOGS_FOLDER, c.LOGS_FILE)
|
|
97
|
+
log_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
|
|
99
|
+
# Rotating file handler
|
|
100
|
+
file_handler = RotatingFileHandler(
|
|
101
|
+
log_file_path,
|
|
102
|
+
maxBytes=log_file_size_mb * 1024 * 1024,
|
|
103
|
+
backupCount=log_file_backup_count
|
|
104
|
+
)
|
|
105
|
+
file_handler.setLevel(log_level.upper())
|
|
106
|
+
file_handler.setFormatter(file_formatter)
|
|
107
|
+
logger.addHandler(file_handler)
|
|
108
|
+
|
|
109
|
+
else:
|
|
110
|
+
stdout_handler = l.StreamHandler()
|
|
111
|
+
stdout_handler.setLevel(log_level.upper())
|
|
112
|
+
stdout_handler.setFormatter(stdout_formatter)
|
|
113
|
+
logger.addHandler(stdout_handler)
|
|
114
|
+
|
|
115
|
+
return logger
|