squirrels 0.4.0__py3-none-any.whl → 0.5.0rc0__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.
- squirrels/__init__.py +10 -6
- squirrels/_api_response_models.py +93 -44
- squirrels/_api_server.py +571 -219
- squirrels/_auth.py +451 -0
- squirrels/_command_line.py +61 -20
- squirrels/_connection_set.py +38 -25
- squirrels/_constants.py +44 -34
- squirrels/_dashboards_io.py +34 -16
- squirrels/_exceptions.py +57 -0
- squirrels/_initializer.py +117 -44
- squirrels/_manifest.py +124 -62
- squirrels/_model_builder.py +111 -0
- squirrels/_model_configs.py +74 -0
- squirrels/_model_queries.py +52 -0
- squirrels/_models.py +860 -354
- squirrels/_package_loader.py +8 -4
- squirrels/_parameter_configs.py +45 -65
- squirrels/_parameter_sets.py +15 -13
- squirrels/_project.py +561 -0
- squirrels/_py_module.py +4 -3
- squirrels/_seeds.py +35 -16
- squirrels/_sources.py +106 -0
- squirrels/_utils.py +166 -63
- squirrels/_version.py +1 -1
- squirrels/arguments/init_time_args.py +78 -15
- squirrels/arguments/run_time_args.py +62 -101
- squirrels/dashboards.py +4 -4
- squirrels/data_sources.py +94 -162
- squirrels/dataset_result.py +86 -0
- squirrels/dateutils.py +4 -4
- squirrels/package_data/base_project/.env +30 -0
- squirrels/package_data/base_project/.env.example +30 -0
- squirrels/package_data/base_project/.gitignore +3 -2
- squirrels/package_data/base_project/assets/expenses.db +0 -0
- squirrels/package_data/base_project/connections.yml +11 -3
- squirrels/package_data/base_project/dashboards/dashboard_example.py +15 -13
- squirrels/package_data/base_project/dashboards/dashboard_example.yml +22 -0
- squirrels/package_data/base_project/docker/.dockerignore +5 -2
- squirrels/package_data/base_project/docker/Dockerfile +3 -3
- squirrels/package_data/base_project/docker/compose.yml +1 -1
- squirrels/package_data/base_project/duckdb_init.sql +9 -0
- squirrels/package_data/base_project/macros/macros_example.sql +15 -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 +55 -0
- squirrels/package_data/base_project/models/dbviews/dbview_example.sql +12 -22
- squirrels/package_data/base_project/models/dbviews/dbview_example.yml +26 -0
- squirrels/package_data/base_project/models/federates/federate_example.py +38 -15
- squirrels/package_data/base_project/models/federates/federate_example.sql +16 -2
- squirrels/package_data/base_project/models/federates/federate_example.yml +65 -0
- squirrels/package_data/base_project/models/sources.yml +39 -0
- squirrels/package_data/base_project/parameters.yml +36 -21
- squirrels/package_data/base_project/pyconfigs/connections.py +6 -11
- squirrels/package_data/base_project/pyconfigs/context.py +20 -33
- squirrels/package_data/base_project/pyconfigs/parameters.py +19 -21
- squirrels/package_data/base_project/pyconfigs/user.py +23 -0
- squirrels/package_data/base_project/seeds/seed_categories.yml +15 -0
- squirrels/package_data/base_project/seeds/seed_subcategories.csv +15 -15
- squirrels/package_data/base_project/seeds/seed_subcategories.yml +21 -0
- squirrels/package_data/base_project/squirrels.yml.j2 +17 -40
- squirrels/parameters.py +20 -20
- {squirrels-0.4.0.dist-info → squirrels-0.5.0rc0.dist-info}/METADATA +31 -32
- squirrels-0.5.0rc0.dist-info/RECORD +70 -0
- {squirrels-0.4.0.dist-info → squirrels-0.5.0rc0.dist-info}/WHEEL +1 -1
- squirrels-0.5.0rc0.dist-info/entry_points.txt +3 -0
- {squirrels-0.4.0.dist-info → squirrels-0.5.0rc0.dist-info/licenses}/LICENSE +1 -1
- squirrels/_authenticator.py +0 -85
- squirrels/_environcfg.py +0 -84
- 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/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/pyconfigs/auth.py +0 -45
- squirrels/package_data/templates/index.html +0 -18
- squirrels/project.py +0 -378
- squirrels/user_base.py +0 -55
- squirrels-0.4.0.dist-info/RECORD +0 -60
- squirrels-0.4.0.dist-info/entry_points.txt +0 -4
squirrels/_initializer.py
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
2
|
from datetime import datetime
|
|
3
|
-
import inquirer, os, shutil
|
|
3
|
+
import inquirer, os, shutil, secrets
|
|
4
4
|
|
|
5
5
|
from . import _constants as c, _utils as u
|
|
6
6
|
|
|
7
7
|
base_proj_dir = u.Path(os.path.dirname(__file__), c.PACKAGE_DATA_FOLDER, c.BASE_PROJECT_FOLDER)
|
|
8
8
|
|
|
9
|
+
TMP_FOLDER = "tmp"
|
|
10
|
+
|
|
9
11
|
|
|
10
12
|
class Initializer:
|
|
11
|
-
def __init__(self, *, overwrite: bool = False):
|
|
13
|
+
def __init__(self, *, project_name: Optional[str] = None, overwrite: bool = False):
|
|
14
|
+
self.project_name = project_name
|
|
12
15
|
self.overwrite = overwrite
|
|
13
16
|
|
|
14
17
|
def _path_exists(self, filepath: u.Path) -> bool:
|
|
@@ -26,27 +29,37 @@ class Initializer:
|
|
|
26
29
|
def _copy_file(self, filepath: u.Path, *, src_folder: str = ""):
|
|
27
30
|
src_path = u.Path(base_proj_dir, src_folder, filepath)
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
filepath2 = u.Path(self.project_name, filepath) if self.project_name else filepath
|
|
33
|
+
dest_dir = os.path.dirname(filepath2)
|
|
30
34
|
if dest_dir != "":
|
|
31
35
|
os.makedirs(dest_dir, exist_ok=True)
|
|
32
36
|
|
|
33
37
|
perform_copy = True
|
|
34
|
-
if self._path_exists(
|
|
35
|
-
old_filepath =
|
|
36
|
-
if self._files_have_same_content(src_path,
|
|
38
|
+
if self._path_exists(filepath2):
|
|
39
|
+
old_filepath = filepath2
|
|
40
|
+
if self._files_have_same_content(src_path, filepath2):
|
|
37
41
|
perform_copy = False
|
|
38
42
|
extra_msg = "Skipping... file contents is same as source"
|
|
39
43
|
elif self.overwrite:
|
|
40
44
|
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(u.Path(c.MACROS_FOLDER, filepath))
|
|
57
|
+
|
|
58
|
+
def _copy_models_file(self, filepath: str):
|
|
59
|
+
self._copy_file(u.Path(c.MODELS_FOLDER, filepath))
|
|
60
|
+
|
|
61
|
+
def _copy_build_file(self, filepath: str):
|
|
62
|
+
self._copy_file(u.Path(c.MODELS_FOLDER, c.BUILDS_FOLDER, filepath))
|
|
50
63
|
|
|
51
64
|
def _copy_dbview_file(self, filepath: str):
|
|
52
65
|
self._copy_file(u.Path(c.MODELS_FOLDER, c.DBVIEWS_FOLDER, filepath))
|
|
@@ -66,34 +79,54 @@ class Initializer:
|
|
|
66
79
|
def _copy_dashboard_file(self, filepath: str):
|
|
67
80
|
self._copy_file(u.Path(c.DASHBOARDS_FOLDER, filepath))
|
|
68
81
|
|
|
69
|
-
def _create_manifest_file(self, has_connections: bool, has_parameters: bool
|
|
70
|
-
TMP_FOLDER = "tmp"
|
|
71
|
-
|
|
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
87
|
yaml_path = u.Path(base_proj_dir, file_name)
|
|
77
|
-
return
|
|
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
98
|
output_path = u.Path(base_proj_dir, TMP_FOLDER, c.MANIFEST_FILE)
|
|
89
|
-
|
|
90
|
-
f.write(manifest_content)
|
|
99
|
+
output_path.write_text(manifest_content)
|
|
91
100
|
|
|
92
101
|
self._copy_file(u.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 = u.Path(base_proj_dir, c.DOTENV_FILE)
|
|
110
|
+
contents = u.render_string(dotenv_path.read_text(), **substitutions)
|
|
111
|
+
|
|
112
|
+
output_path = u.Path(base_proj_dir, TMP_FOLDER, c.DOTENV_FILE)
|
|
113
|
+
output_path.write_text(contents)
|
|
114
|
+
|
|
115
|
+
self._copy_file(u.Path(c.DOTENV_FILE), src_folder=TMP_FOLDER)
|
|
116
|
+
self._copy_file(u.Path(c.DOTENV_FILE + ".example"))
|
|
93
117
|
|
|
94
118
|
def init_project(self, args):
|
|
95
|
-
options = ["core", "connections", "parameters", "
|
|
96
|
-
_, CONNECTIONS, PARAMETERS,
|
|
119
|
+
options = ["core", "connections", "parameters", "build", "federate", "dashboard"]
|
|
120
|
+
_, CONNECTIONS, PARAMETERS, BUILD, FEDERATE, DASHBOARD = options
|
|
121
|
+
|
|
122
|
+
# Add project name prompt if not provided
|
|
123
|
+
if self.project_name is None:
|
|
124
|
+
questions = [
|
|
125
|
+
inquirer.Text('project_name', message="What is your project name? (leave blank to create in current directory)")
|
|
126
|
+
]
|
|
127
|
+
answers = inquirer.prompt(questions)
|
|
128
|
+
assert isinstance(answers, dict)
|
|
129
|
+
self.project_name = answers['project_name']
|
|
97
130
|
|
|
98
131
|
answers = { x: getattr(args, x) for x in options }
|
|
99
132
|
if not any(answers.values()):
|
|
@@ -105,7 +138,7 @@ class Initializer:
|
|
|
105
138
|
PARAMETERS, message=f"How would you like to configure the parameters?", choices=c.CONF_FORMAT_CHOICES2
|
|
106
139
|
),
|
|
107
140
|
inquirer.List(
|
|
108
|
-
|
|
141
|
+
BUILD, message="What's the file format for the build model?", choices=c.FILE_TYPE_CHOICES
|
|
109
142
|
),
|
|
110
143
|
inquirer.List(
|
|
111
144
|
FEDERATE, message="What's the file format for the federated model?", choices=c.FILE_TYPE_CHOICES
|
|
@@ -113,8 +146,8 @@ class Initializer:
|
|
|
113
146
|
inquirer.Confirm(
|
|
114
147
|
DASHBOARD, message=f"Do you want to include a dashboard example?", default=False
|
|
115
148
|
),
|
|
116
|
-
inquirer.
|
|
117
|
-
|
|
149
|
+
inquirer.Password(
|
|
150
|
+
"admin_password", message="What's the admin password? (leave blank to generate a random one)"
|
|
118
151
|
),
|
|
119
152
|
]
|
|
120
153
|
answers = inquirer.prompt(questions)
|
|
@@ -133,6 +166,8 @@ class Initializer:
|
|
|
133
166
|
answer = answers.get(key)
|
|
134
167
|
return answer if answer is not None else default
|
|
135
168
|
|
|
169
|
+
admin_password = get_answer("admin_password", None)
|
|
170
|
+
|
|
136
171
|
connections_format = get_answer(CONNECTIONS, c.YML_FORMAT)
|
|
137
172
|
connections_use_yaml = (connections_format == c.YML_FORMAT)
|
|
138
173
|
connections_use_py = (connections_format == c.PYTHON_FORMAT)
|
|
@@ -141,14 +176,19 @@ class Initializer:
|
|
|
141
176
|
parameters_use_yaml = (parameters_format == c.YML_FORMAT)
|
|
142
177
|
parameters_use_py = (parameters_format == c.PYTHON_FORMAT)
|
|
143
178
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
179
|
+
build_config_file = c.BUILD_FILE_STEM + ".yml"
|
|
180
|
+
build_format = get_answer(BUILD, c.PYTHON_FILE_TYPE)
|
|
181
|
+
if build_format == c.SQL_FILE_TYPE:
|
|
182
|
+
build_file = c.BUILD_FILE_STEM + ".sql"
|
|
183
|
+
elif build_format == c.PYTHON_FILE_TYPE:
|
|
184
|
+
build_file = c.BUILD_FILE_STEM + ".py"
|
|
149
185
|
else:
|
|
150
|
-
raise NotImplementedError(f"
|
|
186
|
+
raise NotImplementedError(f"Build model format '{build_format}' not supported")
|
|
187
|
+
|
|
188
|
+
db_view_config_file = c.DBVIEW_FILE_STEM + ".yml"
|
|
189
|
+
db_view_file = c.DBVIEW_FILE_STEM + ".sql"
|
|
151
190
|
|
|
191
|
+
federate_config_file = c.FEDERATE_FILE_STEM + ".yml"
|
|
152
192
|
federate_format = get_answer(FEDERATE, c.SQL_FILE_TYPE)
|
|
153
193
|
if federate_format == c.SQL_FILE_TYPE:
|
|
154
194
|
federate_file = c.FEDERATE_FILE_STEM + ".sql"
|
|
@@ -159,10 +199,10 @@ class Initializer:
|
|
|
159
199
|
|
|
160
200
|
dashboards_enabled = get_answer(DASHBOARD, False)
|
|
161
201
|
|
|
162
|
-
self.
|
|
202
|
+
self._copy_dotenv_files(admin_password)
|
|
203
|
+
self._create_manifest_file(connections_use_yaml, parameters_use_yaml)
|
|
163
204
|
|
|
164
|
-
self._copy_file(u.Path(
|
|
165
|
-
self._copy_file(u.Path(c.ENV_CONFIG_FILE))
|
|
205
|
+
self._copy_file(u.Path(c.GITIGNORE_FILE))
|
|
166
206
|
|
|
167
207
|
if connections_use_py:
|
|
168
208
|
self._copy_pyconfig_file(c.CONNECTIONS_FILE)
|
|
@@ -178,44 +218,77 @@ class Initializer:
|
|
|
178
218
|
else:
|
|
179
219
|
raise NotImplementedError(f"Format '{parameters_format}' not supported for configuring widget parameters")
|
|
180
220
|
|
|
221
|
+
self._copy_pyconfig_file(c.USER_FILE)
|
|
222
|
+
|
|
181
223
|
self._copy_pyconfig_file(c.CONTEXT_FILE)
|
|
182
|
-
self._copy_seed_file(c.
|
|
183
|
-
self._copy_seed_file(c.
|
|
224
|
+
self._copy_seed_file(c.SEED_CATEGORY_FILE_STEM + ".csv")
|
|
225
|
+
self._copy_seed_file(c.SEED_CATEGORY_FILE_STEM + ".yml")
|
|
226
|
+
self._copy_seed_file(c.SEED_SUBCATEGORY_FILE_STEM + ".csv")
|
|
227
|
+
self._copy_seed_file(c.SEED_SUBCATEGORY_FILE_STEM + ".yml")
|
|
228
|
+
|
|
229
|
+
self._copy_macros_file(c.MACROS_FILE)
|
|
184
230
|
|
|
231
|
+
self._copy_models_file(c.SOURCES_FILE)
|
|
232
|
+
self._copy_build_file(build_file)
|
|
233
|
+
self._copy_build_file(build_config_file)
|
|
185
234
|
self._copy_dbview_file(db_view_file)
|
|
235
|
+
self._copy_dbview_file(db_view_config_file)
|
|
186
236
|
self._copy_federate_file(federate_file)
|
|
237
|
+
self._copy_federate_file(federate_config_file)
|
|
187
238
|
|
|
188
239
|
if dashboards_enabled:
|
|
189
240
|
self._copy_dashboard_file(c.DASHBOARD_FILE_STEM + ".py")
|
|
241
|
+
self._copy_dashboard_file(c.DASHBOARD_FILE_STEM + ".yml")
|
|
190
242
|
|
|
191
|
-
if get_answer(AUTH, False):
|
|
192
|
-
self._copy_pyconfig_file(c.AUTH_FILE)
|
|
193
|
-
|
|
194
243
|
self._copy_database_file(c.EXPENSES_DB)
|
|
195
244
|
|
|
196
245
|
print(f"\nSuccessfully created new Squirrels project in current directory!\n")
|
|
197
246
|
|
|
198
247
|
def get_file(self, args):
|
|
199
|
-
if args.file_name == c.
|
|
200
|
-
self.
|
|
201
|
-
print("
|
|
248
|
+
if args.file_name == c.DOTENV_FILE:
|
|
249
|
+
self._copy_dotenv_files()
|
|
250
|
+
print(f"A random admin password was generated for your project. You can change it in the new {c.DOTENV_FILE} file.")
|
|
251
|
+
print()
|
|
252
|
+
print(f"IMPORTANT: Please ensure the {c.DOTENV_FILE} file is added to your {c.GITIGNORE_FILE} file.")
|
|
253
|
+
print(f"You may also run `sqrl get-file {c.GITIGNORE_FILE}` to add a sample {c.GITIGNORE_FILE} file to your project.")
|
|
254
|
+
print()
|
|
255
|
+
elif args.file_name == c.GITIGNORE_FILE:
|
|
256
|
+
self._copy_file(u.Path(c.GITIGNORE_FILE))
|
|
202
257
|
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.
|
|
258
|
+
self._create_manifest_file(not args.no_connections, args.parameters)
|
|
259
|
+
elif args.file_name in (c.USER_FILE, c.CONNECTIONS_FILE, c.PARAMETERS_FILE, c.CONTEXT_FILE):
|
|
205
260
|
self._copy_pyconfig_file(args.file_name)
|
|
206
|
-
elif args.file_name
|
|
207
|
-
|
|
261
|
+
elif args.file_name == c.MACROS_FILE:
|
|
262
|
+
self._copy_macros_file(args.file_name)
|
|
263
|
+
elif args.file_name == c.SOURCES_FILE:
|
|
264
|
+
self._copy_models_file(args.file_name)
|
|
265
|
+
elif args.file_name in (c.BUILD_FILE_STEM, c.DBVIEW_FILE_STEM, c.FEDERATE_FILE_STEM):
|
|
266
|
+
if args.file_name == c.DBVIEW_FILE_STEM or args.format == c.SQL_FILE_TYPE:
|
|
208
267
|
extension = ".sql"
|
|
209
268
|
elif args.format == c.PYTHON_FILE_TYPE:
|
|
210
269
|
extension = ".py"
|
|
211
270
|
else:
|
|
212
271
|
raise NotImplementedError(f"Format '{args.format}' not supported for {args.file_name}")
|
|
213
|
-
|
|
272
|
+
|
|
273
|
+
if args.file_name == c.BUILD_FILE_STEM:
|
|
274
|
+
copy_method = self._copy_build_file
|
|
275
|
+
elif args.file_name == c.DBVIEW_FILE_STEM:
|
|
276
|
+
copy_method = self._copy_dbview_file
|
|
277
|
+
elif args.file_name == c.FEDERATE_FILE_STEM:
|
|
278
|
+
copy_method = self._copy_federate_file
|
|
279
|
+
else:
|
|
280
|
+
raise NotImplementedError(f"File '{args.file_name}' not supported")
|
|
281
|
+
|
|
214
282
|
copy_method(args.file_name + extension)
|
|
283
|
+
copy_method(args.file_name + ".yml")
|
|
215
284
|
elif args.file_name == c.DASHBOARD_FILE_STEM:
|
|
216
285
|
self._copy_dashboard_file(args.file_name + ".py")
|
|
286
|
+
self._copy_dashboard_file(args.file_name + ".yml")
|
|
217
287
|
elif args.file_name in (c.EXPENSES_DB, c.WEATHER_DB):
|
|
218
288
|
self._copy_database_file(args.file_name)
|
|
289
|
+
elif args.file_name in (c.SEED_CATEGORY_FILE_STEM, c.SEED_SUBCATEGORY_FILE_STEM):
|
|
290
|
+
self._copy_seed_file(args.file_name + ".csv")
|
|
291
|
+
self._copy_seed_file(args.file_name + ".yml")
|
|
219
292
|
else:
|
|
220
293
|
raise NotImplementedError(f"File '{args.file_name}' not supported")
|
|
221
294
|
|
squirrels/_manifest.py
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
|
+
from functools import cached_property
|
|
1
2
|
from typing import Any
|
|
3
|
+
from urllib.parse import urlparse
|
|
4
|
+
from sqlalchemy import Engine, create_engine
|
|
2
5
|
from typing_extensions import Self
|
|
3
6
|
from enum import Enum
|
|
4
7
|
from pydantic import BaseModel, Field, field_validator, model_validator, ValidationInfo, ValidationError
|
|
5
8
|
import yaml, time
|
|
6
9
|
|
|
7
|
-
from . import _constants as c, _utils as
|
|
8
|
-
from ._environcfg import EnvironConfig
|
|
10
|
+
from . import _constants as c, _utils as u
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
class ProjectVarsConfig(BaseModel, extra="allow"):
|
|
12
14
|
name: str
|
|
13
15
|
label: str = ""
|
|
16
|
+
description: str = ""
|
|
14
17
|
major_version: int
|
|
15
18
|
|
|
16
19
|
@model_validator(mode="after")
|
|
@@ -36,13 +39,78 @@ class _ConfigWithNameBaseModel(BaseModel):
|
|
|
36
39
|
name: str
|
|
37
40
|
|
|
38
41
|
|
|
39
|
-
class
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
class ConnectionType(Enum):
|
|
43
|
+
SQLALCHEMY = "sqlalchemy"
|
|
44
|
+
CONNECTORX = "connectorx"
|
|
45
|
+
ADBC = "adbc"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ConnectionProperties(BaseModel):
|
|
49
|
+
"""
|
|
50
|
+
A class for holding the properties of a connection
|
|
51
|
+
|
|
52
|
+
Arguments:
|
|
53
|
+
type: The type of connection, one of "sqlalchemy", "connectorx", or "adbc"
|
|
54
|
+
uri: The URI for the connection
|
|
55
|
+
"""
|
|
56
|
+
label: str | None = None
|
|
57
|
+
type: ConnectionType = Field(default=ConnectionType.SQLALCHEMY)
|
|
58
|
+
uri: str
|
|
59
|
+
sa_create_engine_args: dict[str, Any] = Field(default_factory=dict)
|
|
60
|
+
|
|
61
|
+
@cached_property
|
|
62
|
+
def engine(self) -> Engine:
|
|
63
|
+
"""
|
|
64
|
+
Creates and caches a SQLAlchemy engine if the connection type is sqlalchemy.
|
|
65
|
+
Returns None for other connection types.
|
|
66
|
+
"""
|
|
67
|
+
if self.type == ConnectionType.SQLALCHEMY:
|
|
68
|
+
return create_engine(self.uri, **self.sa_create_engine_args)
|
|
69
|
+
else:
|
|
70
|
+
raise ValueError(f'Connection type "{self.type}" does not support engine property')
|
|
42
71
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
self.
|
|
72
|
+
@cached_property
|
|
73
|
+
def dialect(self) -> str:
|
|
74
|
+
if self.type == ConnectionType.SQLALCHEMY:
|
|
75
|
+
dialect = self.engine.dialect.name
|
|
76
|
+
else:
|
|
77
|
+
url = urlparse(self.uri)
|
|
78
|
+
dialect = url.scheme
|
|
79
|
+
|
|
80
|
+
processed_dialect = next((d for d in ['sqlite', 'postgres', 'mysql'] if dialect.lower().startswith(d)), None)
|
|
81
|
+
dialect = processed_dialect if processed_dialect is not None else dialect
|
|
82
|
+
return dialect
|
|
83
|
+
|
|
84
|
+
@cached_property
|
|
85
|
+
def attach_uri_for_duckdb(self) -> str | None:
|
|
86
|
+
if self.type == ConnectionType.SQLALCHEMY:
|
|
87
|
+
url = self.engine.url
|
|
88
|
+
host = url.host
|
|
89
|
+
port = url.port
|
|
90
|
+
username = url.username
|
|
91
|
+
password = url.password
|
|
92
|
+
database = url.database
|
|
93
|
+
sqlite_database = database if database is not None else ""
|
|
94
|
+
else:
|
|
95
|
+
url = urlparse(self.uri)
|
|
96
|
+
host = url.hostname
|
|
97
|
+
port = url.port
|
|
98
|
+
username = url.username
|
|
99
|
+
password = url.password
|
|
100
|
+
database = url.path.lstrip('/')
|
|
101
|
+
sqlite_database = self.uri.replace(f"{self.dialect}://", "")
|
|
102
|
+
|
|
103
|
+
if self.dialect == 'sqlite':
|
|
104
|
+
return sqlite_database
|
|
105
|
+
elif self.dialect in ('postgres', 'mysql'):
|
|
106
|
+
return f"dbname={database} user={username} password={password} host={host} port={port}"
|
|
107
|
+
else:
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class DbConnConfig(ConnectionProperties, _ConfigWithNameBaseModel):
|
|
112
|
+
def finalize_uri(self, base_path: str) -> Self:
|
|
113
|
+
self.uri = self.uri.format(project_path=base_path)
|
|
46
114
|
return self
|
|
47
115
|
|
|
48
116
|
|
|
@@ -52,15 +120,7 @@ class ParametersConfig(BaseModel):
|
|
|
52
120
|
arguments: dict[str, Any]
|
|
53
121
|
|
|
54
122
|
|
|
55
|
-
class
|
|
56
|
-
connection_name: str | None = None
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
class FederateConfig(_ConfigWithNameBaseModel):
|
|
60
|
-
materialized: str | None = None
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
class DatasetScope(Enum):
|
|
123
|
+
class PermissionScope(Enum):
|
|
64
124
|
PUBLIC = 0
|
|
65
125
|
PROTECTED = 1
|
|
66
126
|
PRIVATE = 2
|
|
@@ -69,8 +129,8 @@ class DatasetScope(Enum):
|
|
|
69
129
|
class AnalyticsOutputConfig(_ConfigWithNameBaseModel):
|
|
70
130
|
label: str = ""
|
|
71
131
|
description: str = ""
|
|
72
|
-
scope:
|
|
73
|
-
parameters: list[str] = Field(
|
|
132
|
+
scope: PermissionScope = PermissionScope.PUBLIC
|
|
133
|
+
parameters: list[str] | None = Field(default=None, description="The list of parameter names used by the dataset/dashboard")
|
|
74
134
|
|
|
75
135
|
@model_validator(mode="after")
|
|
76
136
|
def finalize_label(self) -> Self:
|
|
@@ -80,15 +140,19 @@ class AnalyticsOutputConfig(_ConfigWithNameBaseModel):
|
|
|
80
140
|
|
|
81
141
|
@field_validator("scope", mode="before")
|
|
82
142
|
@classmethod
|
|
83
|
-
def validate_scope(cls, value: str, info: ValidationInfo) ->
|
|
143
|
+
def validate_scope(cls, value: str, info: ValidationInfo) -> PermissionScope:
|
|
84
144
|
try:
|
|
85
|
-
return
|
|
145
|
+
return PermissionScope[str(value).upper()]
|
|
86
146
|
except KeyError as e:
|
|
87
147
|
name = info.data.get("name")
|
|
88
|
-
scope_list = [scope.name.lower() for scope in
|
|
148
|
+
scope_list = [scope.name.lower() for scope in PermissionScope]
|
|
89
149
|
raise ValueError(f'Scope "{value}" is invalid for dataset/dashboard "{name}". Scope must be one of {scope_list}') from e
|
|
90
150
|
|
|
91
151
|
|
|
152
|
+
class DatasetTraitConfig(_ConfigWithNameBaseModel):
|
|
153
|
+
default: Any
|
|
154
|
+
|
|
155
|
+
|
|
92
156
|
class DatasetConfig(AnalyticsOutputConfig):
|
|
93
157
|
model: str = ""
|
|
94
158
|
traits: dict = Field(default_factory=dict)
|
|
@@ -104,47 +168,26 @@ class DatasetConfig(AnalyticsOutputConfig):
|
|
|
104
168
|
return self
|
|
105
169
|
|
|
106
170
|
|
|
107
|
-
class DashboardConfig(AnalyticsOutputConfig):
|
|
108
|
-
def __hash__(self) -> int:
|
|
109
|
-
return hash("dashboard_"+self.name)
|
|
110
|
-
|
|
111
|
-
|
|
112
171
|
class TestSetsConfig(_ConfigWithNameBaseModel):
|
|
113
172
|
datasets: list[str] | None = None
|
|
114
|
-
|
|
115
|
-
user_attributes: dict[str, Any] = Field(default_factory=dict)
|
|
173
|
+
user_attributes: dict[str, Any] | None = None
|
|
116
174
|
parameters: dict[str, Any] = Field(default_factory=dict)
|
|
117
175
|
|
|
118
|
-
@
|
|
119
|
-
def
|
|
120
|
-
|
|
121
|
-
self.is_authenticated = True
|
|
122
|
-
return self
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
class Settings(BaseModel):
|
|
126
|
-
data: dict[str, Any]
|
|
127
|
-
|
|
128
|
-
def get_default_connection_name(self) -> str:
|
|
129
|
-
return self.data.get(c.DB_CONN_DEFAULT_USED_SETTING, c.DEFAULT_DB_CONN)
|
|
130
|
-
|
|
131
|
-
def do_use_duckdb(self) -> bool:
|
|
132
|
-
return self.data.get(c.IN_MEMORY_DB_SETTING, c.SQLITE) == c.DUCKDB
|
|
176
|
+
@property
|
|
177
|
+
def is_authenticated(self) -> bool:
|
|
178
|
+
return self.user_attributes is not None
|
|
133
179
|
|
|
134
180
|
|
|
135
181
|
class ManifestConfig(BaseModel):
|
|
136
|
-
env_cfg: EnvironConfig
|
|
137
182
|
project_variables: ProjectVarsConfig
|
|
138
183
|
packages: list[PackageConfig] = Field(default_factory=list)
|
|
139
184
|
connections: dict[str, DbConnConfig] = Field(default_factory=dict)
|
|
140
185
|
parameters: list[ParametersConfig] = Field(default_factory=list)
|
|
141
186
|
selection_test_sets: dict[str, TestSetsConfig] = Field(default_factory=dict)
|
|
142
|
-
|
|
143
|
-
federates: dict[str, FederateConfig] = Field(default_factory=dict)
|
|
187
|
+
dataset_traits: dict[str, DatasetTraitConfig] = Field(default_factory=dict)
|
|
144
188
|
datasets: dict[str, DatasetConfig] = Field(default_factory=dict)
|
|
145
|
-
dashboards: dict[str, DashboardConfig] = Field(default_factory=dict)
|
|
146
|
-
settings: dict[str, Any] = Field(default_factory=dict)
|
|
147
189
|
base_path: str = "."
|
|
190
|
+
env_vars: dict[str, str] = Field(default_factory=dict)
|
|
148
191
|
|
|
149
192
|
@field_validator("packages")
|
|
150
193
|
@classmethod
|
|
@@ -156,7 +199,7 @@ class ManifestConfig(BaseModel):
|
|
|
156
199
|
set_of_directories.add(package.directory)
|
|
157
200
|
return packages
|
|
158
201
|
|
|
159
|
-
@field_validator("connections", "selection_test_sets", "
|
|
202
|
+
@field_validator("connections", "selection_test_sets", "dataset_traits", "datasets", mode="before")
|
|
160
203
|
@classmethod
|
|
161
204
|
def names_are_unique(cls, values: list[dict] | dict[str, dict], info: ValidationInfo) -> dict[str, dict]:
|
|
162
205
|
if isinstance(values, list):
|
|
@@ -173,19 +216,33 @@ class ManifestConfig(BaseModel):
|
|
|
173
216
|
@model_validator(mode="after")
|
|
174
217
|
def finalize_connections(self) -> Self:
|
|
175
218
|
for conn in self.connections.values():
|
|
176
|
-
conn.
|
|
219
|
+
conn.finalize_uri(self.base_path)
|
|
220
|
+
return self
|
|
221
|
+
|
|
222
|
+
@model_validator(mode="after")
|
|
223
|
+
def validate_dataset_traits(self) -> Self:
|
|
224
|
+
for dataset_name, dataset in self.datasets.items():
|
|
225
|
+
# Validate that all trait keys in dataset.traits exist in dataset_traits
|
|
226
|
+
for trait_key in dataset.traits.keys():
|
|
227
|
+
if trait_key not in self.dataset_traits:
|
|
228
|
+
raise ValueError(
|
|
229
|
+
f'Dataset "{dataset_name}" references undefined trait "{trait_key}". '
|
|
230
|
+
f'Traits must be defined with a default value in the dataset_traits section.'
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Set default values for any traits that are missing
|
|
234
|
+
for trait_name, trait_config in self.dataset_traits.items():
|
|
235
|
+
if trait_name not in dataset.traits:
|
|
236
|
+
dataset.traits[trait_name] = trait_config.default
|
|
237
|
+
|
|
177
238
|
return self
|
|
178
239
|
|
|
179
|
-
@property
|
|
180
|
-
def settings_obj(self) -> Settings:
|
|
181
|
-
return Settings(data=self.settings)
|
|
182
|
-
|
|
183
240
|
def get_default_test_set(self, dataset_name: str) -> TestSetsConfig:
|
|
184
241
|
"""
|
|
185
242
|
Raises KeyError if dataset name doesn't exist
|
|
186
243
|
"""
|
|
187
244
|
default_name_1 = self.datasets[dataset_name].default_test_set
|
|
188
|
-
default_name_2 = self.
|
|
245
|
+
default_name_2 = self.env_vars.get(c.SQRL_TEST_SETS_DEFAULT_NAME_USED, "default")
|
|
189
246
|
default_name = default_name_1 if default_name_1 else default_name_2
|
|
190
247
|
default_test_set = self.selection_test_sets.get(default_name, TestSetsConfig(name=default_name))
|
|
191
248
|
return default_test_set
|
|
@@ -196,22 +253,27 @@ class ManifestConfig(BaseModel):
|
|
|
196
253
|
if test_set_config.datasets is None or dataset in test_set_config.datasets:
|
|
197
254
|
applicable_test_sets.append(test_set_name)
|
|
198
255
|
return applicable_test_sets
|
|
256
|
+
|
|
257
|
+
def get_default_traits(self) -> dict[str, Any]:
|
|
258
|
+
default_traits = {}
|
|
259
|
+
for trait_name, trait_config in self.dataset_traits.items():
|
|
260
|
+
default_traits[trait_name] = trait_config.default
|
|
261
|
+
return default_traits
|
|
199
262
|
|
|
200
263
|
|
|
201
264
|
class ManifestIO:
|
|
202
265
|
|
|
203
266
|
@classmethod
|
|
204
|
-
def load_from_file(cls, logger:
|
|
267
|
+
def load_from_file(cls, logger: u.Logger, base_path: str, env_vars: dict[str, str]) -> ManifestConfig:
|
|
205
268
|
start = time.time()
|
|
206
269
|
|
|
207
|
-
raw_content =
|
|
208
|
-
|
|
209
|
-
content = _u.render_string(raw_content, base_path=base_path, env_vars=env_vars)
|
|
270
|
+
raw_content = u.read_file(u.Path(base_path, c.MANIFEST_FILE))
|
|
271
|
+
content = u.render_string(raw_content, base_path=base_path, env_vars=env_vars)
|
|
210
272
|
manifest_content = yaml.safe_load(content)
|
|
211
273
|
try:
|
|
212
|
-
manifest_cfg = ManifestConfig(base_path=base_path,
|
|
274
|
+
manifest_cfg = ManifestConfig(base_path=base_path, **manifest_content)
|
|
213
275
|
except ValidationError as e:
|
|
214
|
-
raise
|
|
276
|
+
raise u.ConfigurationError(f"Failed to process {c.MANIFEST_FILE} file. " + str(e)) from e
|
|
215
277
|
|
|
216
278
|
logger.log_activity_time(f"loading {c.MANIFEST_FILE} file", start)
|
|
217
279
|
return manifest_cfg
|