squirrels 0.4.0__py3-none-any.whl → 0.5.0b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of squirrels might be problematic. Click here for more details.

Files changed (80) hide show
  1. squirrels/__init__.py +10 -6
  2. squirrels/_api_response_models.py +93 -44
  3. squirrels/_api_server.py +571 -219
  4. squirrels/_auth.py +451 -0
  5. squirrels/_command_line.py +61 -20
  6. squirrels/_connection_set.py +38 -25
  7. squirrels/_constants.py +44 -34
  8. squirrels/_dashboards_io.py +34 -16
  9. squirrels/_exceptions.py +57 -0
  10. squirrels/_initializer.py +117 -44
  11. squirrels/_manifest.py +124 -62
  12. squirrels/_model_builder.py +111 -0
  13. squirrels/_model_configs.py +74 -0
  14. squirrels/_model_queries.py +52 -0
  15. squirrels/_models.py +860 -354
  16. squirrels/_package_loader.py +8 -4
  17. squirrels/_parameter_configs.py +45 -65
  18. squirrels/_parameter_sets.py +15 -13
  19. squirrels/_project.py +561 -0
  20. squirrels/_py_module.py +4 -3
  21. squirrels/_seeds.py +35 -16
  22. squirrels/_sources.py +106 -0
  23. squirrels/_utils.py +166 -63
  24. squirrels/_version.py +1 -1
  25. squirrels/arguments/init_time_args.py +78 -15
  26. squirrels/arguments/run_time_args.py +62 -101
  27. squirrels/dashboards.py +4 -4
  28. squirrels/data_sources.py +94 -162
  29. squirrels/dataset_result.py +86 -0
  30. squirrels/dateutils.py +4 -4
  31. squirrels/package_data/base_project/.env +30 -0
  32. squirrels/package_data/base_project/.env.example +30 -0
  33. squirrels/package_data/base_project/.gitignore +3 -2
  34. squirrels/package_data/base_project/assets/expenses.db +0 -0
  35. squirrels/package_data/base_project/connections.yml +11 -3
  36. squirrels/package_data/base_project/dashboards/dashboard_example.py +15 -13
  37. squirrels/package_data/base_project/dashboards/dashboard_example.yml +22 -0
  38. squirrels/package_data/base_project/docker/.dockerignore +5 -2
  39. squirrels/package_data/base_project/docker/Dockerfile +3 -3
  40. squirrels/package_data/base_project/docker/compose.yml +1 -1
  41. squirrels/package_data/base_project/duckdb_init.sql +9 -0
  42. squirrels/package_data/base_project/macros/macros_example.sql +15 -0
  43. squirrels/package_data/base_project/models/builds/build_example.py +26 -0
  44. squirrels/package_data/base_project/models/builds/build_example.sql +16 -0
  45. squirrels/package_data/base_project/models/builds/build_example.yml +55 -0
  46. squirrels/package_data/base_project/models/dbviews/dbview_example.sql +12 -22
  47. squirrels/package_data/base_project/models/dbviews/dbview_example.yml +26 -0
  48. squirrels/package_data/base_project/models/federates/federate_example.py +38 -15
  49. squirrels/package_data/base_project/models/federates/federate_example.sql +16 -2
  50. squirrels/package_data/base_project/models/federates/federate_example.yml +65 -0
  51. squirrels/package_data/base_project/models/sources.yml +39 -0
  52. squirrels/package_data/base_project/parameters.yml +36 -21
  53. squirrels/package_data/base_project/pyconfigs/connections.py +6 -11
  54. squirrels/package_data/base_project/pyconfigs/context.py +20 -33
  55. squirrels/package_data/base_project/pyconfigs/parameters.py +19 -21
  56. squirrels/package_data/base_project/pyconfigs/user.py +23 -0
  57. squirrels/package_data/base_project/seeds/seed_categories.yml +15 -0
  58. squirrels/package_data/base_project/seeds/seed_subcategories.csv +15 -15
  59. squirrels/package_data/base_project/seeds/seed_subcategories.yml +21 -0
  60. squirrels/package_data/base_project/squirrels.yml.j2 +17 -40
  61. squirrels/parameters.py +20 -20
  62. {squirrels-0.4.0.dist-info → squirrels-0.5.0b1.dist-info}/METADATA +31 -32
  63. squirrels-0.5.0b1.dist-info/RECORD +70 -0
  64. {squirrels-0.4.0.dist-info → squirrels-0.5.0b1.dist-info}/WHEEL +1 -1
  65. squirrels-0.5.0b1.dist-info/entry_points.txt +3 -0
  66. {squirrels-0.4.0.dist-info → squirrels-0.5.0b1.dist-info/licenses}/LICENSE +1 -1
  67. squirrels/_authenticator.py +0 -85
  68. squirrels/_environcfg.py +0 -84
  69. squirrels/package_data/assets/favicon.ico +0 -0
  70. squirrels/package_data/assets/index.css +0 -1
  71. squirrels/package_data/assets/index.js +0 -58
  72. squirrels/package_data/base_project/dashboards.yml +0 -10
  73. squirrels/package_data/base_project/env.yml +0 -29
  74. squirrels/package_data/base_project/models/dbviews/dbview_example.py +0 -47
  75. squirrels/package_data/base_project/pyconfigs/auth.py +0 -45
  76. squirrels/package_data/templates/index.html +0 -18
  77. squirrels/project.py +0 -378
  78. squirrels/user_base.py +0 -55
  79. squirrels-0.4.0.dist-info/RECORD +0 -60
  80. 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
- dest_dir = os.path.dirname(filepath)
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(filepath):
35
- old_filepath = filepath
36
- if self._files_have_same_content(src_path, filepath):
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
- filepath = self._add_timestamp_to_filename(old_filepath)
43
- extra_msg = f'Creating file as "{filepath}" instead...'
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 "{filepath}"...')
50
+ print(f'Creating file "{filepath2}"...')
47
51
 
48
52
  if perform_copy:
49
- shutil.copy(src_path, filepath)
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, has_dashboards: 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 u.read_file(yaml_path)
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
- with open(u.Path(output_path), "w") as f:
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", "dbview", "federate", "dashboard", "auth"]
96
- _, CONNECTIONS, PARAMETERS, DBVIEW, FEDERATE, DASHBOARD, AUTH = options
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
- DBVIEW, message="What's the file format for the database view model?", choices=c.FILE_TYPE_CHOICES
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.Confirm(
117
- AUTH, message=f"Do you want to add the '{c.AUTH_FILE}' file to enable custom API authentication?", default=False
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
- dbview_format = get_answer(DBVIEW, c.SQL_FILE_TYPE)
145
- if dbview_format == c.SQL_FILE_TYPE:
146
- db_view_file = c.DBVIEW_FILE_STEM + ".sql"
147
- elif dbview_format == c.PYTHON_FILE_TYPE:
148
- db_view_file = c.DBVIEW_FILE_STEM + ".py"
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"Dbview model format '{dbview_format}' not supported")
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._create_manifest_file(connections_use_yaml, parameters_use_yaml, dashboards_enabled)
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(".gitignore"))
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.CATEGORY_SEED_FILE)
183
- self._copy_seed_file(c.SUBCATEGORY_SEED_FILE)
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.ENV_CONFIG_FILE:
200
- self._copy_file(u.Path(c.ENV_CONFIG_FILE))
201
- print("PLEASE ENSURE THE FILE IS INCLUDED IN .gitignore")
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, args.dashboards)
204
- elif args.file_name in (c.AUTH_FILE, c.CONNECTIONS_FILE, c.PARAMETERS_FILE, c.CONTEXT_FILE):
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 in (c.DBVIEW_FILE_STEM, c.FEDERATE_FILE_STEM):
207
- if args.format == c.SQL_FILE_TYPE:
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
- copy_method = self._copy_dbview_file if args.file_name == c.DBVIEW_FILE_STEM else self._copy_federate_file
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 _u
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 DbConnConfig(_ConfigWithNameBaseModel):
40
- credential: str | None = None
41
- url: str
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
- def finalize_url(self, base_path: str, env_cfg: EnvironConfig) -> Self:
44
- username, password = env_cfg.get_credential(self.credential)
45
- self.url = self.url.format(username=username, password=password, project_path=base_path)
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 DbviewConfig(_ConfigWithNameBaseModel):
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: DatasetScope = DatasetScope.PUBLIC
73
- parameters: list[str] = Field(default_factory=list)
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) -> DatasetScope:
143
+ def validate_scope(cls, value: str, info: ValidationInfo) -> PermissionScope:
84
144
  try:
85
- return DatasetScope[str(value).upper()]
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 DatasetScope]
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
- is_authenticated: bool = False
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
- @model_validator(mode="after")
119
- def finalize_is_authenticated(self) -> Self:
120
- if len(self.user_attributes) > 0:
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
- dbviews: dict[str, DbviewConfig] = Field(default_factory=dict)
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", "dbviews", "federates", "datasets", "dashboards", mode="before")
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.finalize_url(self.base_path, self.env_cfg)
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.settings.get(c.TEST_SET_DEFAULT_USED_SETTING, c.DEFAULT_TEST_SET_NAME)
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: _u.Logger, base_path: str, env_cfg: EnvironConfig) -> ManifestConfig:
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 = _u.read_file(_u.Path(base_path, c.MANIFEST_FILE))
208
- env_vars = env_cfg.get_all_env_vars()
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, env_cfg=env_cfg, **manifest_content)
274
+ manifest_cfg = ManifestConfig(base_path=base_path, **manifest_content)
213
275
  except ValidationError as e:
214
- raise _u.ConfigurationError(f"Failed to process {c.MANIFEST_FILE} file. " + str(e)) from e
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