squirrels 0.5.0b3__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.
- squirrels/__init__.py +4 -0
- 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 +440 -792
- squirrels/_arguments/__init__.py +0 -0
- squirrels/_arguments/{_init_time_args.py → init_time_args.py} +23 -43
- squirrels/_arguments/{_run_time_args.py → run_time_args.py} +32 -68
- squirrels/_auth.py +590 -264
- squirrels/_command_line.py +130 -58
- squirrels/_compile_prompts.py +147 -0
- squirrels/_connection_set.py +16 -15
- squirrels/_constants.py +36 -11
- squirrels/_dashboards.py +179 -0
- squirrels/_data_sources.py +40 -34
- squirrels/_dataset_types.py +16 -11
- squirrels/_env_vars.py +209 -0
- squirrels/_exceptions.py +9 -37
- squirrels/_http_error_responses.py +52 -0
- squirrels/_initializer.py +7 -6
- squirrels/_logging.py +121 -0
- squirrels/_manifest.py +155 -77
- squirrels/_mcp_server.py +578 -0
- squirrels/_model_builder.py +11 -55
- squirrels/_model_configs.py +5 -5
- squirrels/_model_queries.py +1 -1
- squirrels/_models.py +276 -143
- squirrels/_package_data/base_project/.env +1 -24
- squirrels/_package_data/base_project/.env.example +31 -17
- squirrels/_package_data/base_project/connections.yml +4 -3
- squirrels/_package_data/base_project/dashboards/dashboard_example.py +13 -7
- squirrels/_package_data/base_project/dashboards/dashboard_example.yml +6 -6
- squirrels/_package_data/base_project/docker/Dockerfile +2 -2
- squirrels/_package_data/base_project/docker/compose.yml +1 -1
- squirrels/_package_data/base_project/duckdb_init.sql +1 -0
- squirrels/_package_data/base_project/models/builds/build_example.py +2 -2
- squirrels/_package_data/base_project/models/dbviews/dbview_example.sql +7 -2
- squirrels/_package_data/base_project/models/dbviews/dbview_example.yml +16 -10
- squirrels/_package_data/base_project/models/federates/federate_example.py +27 -17
- squirrels/_package_data/base_project/models/federates/federate_example.sql +3 -7
- squirrels/_package_data/base_project/models/federates/federate_example.yml +7 -7
- squirrels/_package_data/base_project/models/sources.yml +5 -6
- squirrels/_package_data/base_project/parameters.yml +24 -38
- squirrels/_package_data/base_project/pyconfigs/connections.py +8 -3
- squirrels/_package_data/base_project/pyconfigs/context.py +26 -14
- squirrels/_package_data/base_project/pyconfigs/parameters.py +124 -81
- squirrels/_package_data/base_project/pyconfigs/user.py +48 -15
- squirrels/_package_data/base_project/resources/public/.gitkeep +0 -0
- squirrels/_package_data/base_project/seeds/seed_categories.yml +1 -1
- squirrels/_package_data/base_project/seeds/seed_subcategories.yml +1 -1
- squirrels/_package_data/base_project/squirrels.yml.j2 +21 -31
- squirrels/_package_data/templates/login_successful.html +53 -0
- squirrels/_package_data/templates/squirrels_studio.html +22 -0
- squirrels/_parameter_configs.py +43 -22
- squirrels/_parameter_options.py +1 -1
- squirrels/_parameter_sets.py +41 -30
- squirrels/_parameters.py +560 -123
- squirrels/_project.py +487 -277
- squirrels/_py_module.py +71 -10
- 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 +52 -13
- squirrels/_sources.py +29 -23
- squirrels/_utils.py +221 -42
- squirrels/_version.py +1 -3
- squirrels/arguments.py +7 -2
- squirrels/auth.py +4 -0
- squirrels/connections.py +2 -0
- squirrels/dashboards.py +3 -1
- squirrels/data_sources.py +6 -0
- squirrels/parameter_options.py +5 -0
- squirrels/parameters.py +5 -0
- squirrels/types.py +10 -3
- squirrels-0.6.0.post0.dist-info/METADATA +148 -0
- squirrels-0.6.0.post0.dist-info/RECORD +101 -0
- {squirrels-0.5.0b3.dist-info → squirrels-0.6.0.post0.dist-info}/WHEEL +1 -1
- squirrels/_api_response_models.py +0 -190
- squirrels/_dashboard_types.py +0 -82
- squirrels/_dashboards_io.py +0 -79
- squirrels-0.5.0b3.dist-info/METADATA +0 -110
- squirrels-0.5.0b3.dist-info/RECORD +0 -80
- /squirrels/_package_data/base_project/{assets → resources}/expenses.db +0 -0
- /squirrels/_package_data/base_project/{assets → resources}/weather.db +0 -0
- {squirrels-0.5.0b3.dist-info → squirrels-0.6.0.post0.dist-info}/entry_points.txt +0 -0
- {squirrels-0.5.0b3.dist-info → squirrels-0.6.0.post0.dist-info}/licenses/LICENSE +0 -0
squirrels/_manifest.py
CHANGED
|
@@ -1,25 +1,58 @@
|
|
|
1
1
|
from functools import cached_property
|
|
2
|
-
from typing import Any
|
|
2
|
+
from typing import Literal, Any
|
|
3
3
|
from urllib.parse import urlparse
|
|
4
4
|
from sqlalchemy import Engine, create_engine
|
|
5
5
|
from typing_extensions import Self
|
|
6
6
|
from enum import Enum
|
|
7
7
|
from pydantic import BaseModel, Field, field_validator, model_validator, ValidationInfo, ValidationError
|
|
8
|
-
import yaml, time
|
|
8
|
+
import yaml, time, re
|
|
9
9
|
|
|
10
10
|
from . import _constants as c, _utils as u
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
class
|
|
13
|
+
class _ConfigWithNameBaseModel(BaseModel):
|
|
14
14
|
name: str
|
|
15
|
+
|
|
16
|
+
@field_validator("name")
|
|
17
|
+
@classmethod
|
|
18
|
+
def validate_name(cls, v: str) -> str:
|
|
19
|
+
if not re.fullmatch(r"[A-Za-z0-9_-]+", v):
|
|
20
|
+
raise ValueError("Name must only contain alphanumeric characters, underscores, and dashes.")
|
|
21
|
+
return v
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AuthType(Enum):
|
|
25
|
+
REQUIRED = "required"
|
|
26
|
+
OPTIONAL = "optional"
|
|
27
|
+
NOTSET = "notset"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AuthStrategy(Enum):
|
|
31
|
+
MANAGED = "managed"
|
|
32
|
+
EXTERNAL = "external"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ProjectVarsConfig(_ConfigWithNameBaseModel, extra="allow"):
|
|
36
|
+
major_version: int
|
|
15
37
|
label: str = ""
|
|
16
38
|
description: str = ""
|
|
17
|
-
|
|
39
|
+
auth_type: AuthType = AuthType.NOTSET
|
|
40
|
+
auth_strategy: AuthStrategy = AuthStrategy.MANAGED
|
|
41
|
+
|
|
42
|
+
@model_validator(mode="after")
|
|
43
|
+
def set_auth_strategy_defaults(self) -> Self:
|
|
44
|
+
if self.auth_strategy == AuthStrategy.EXTERNAL and self.auth_type == AuthType.OPTIONAL:
|
|
45
|
+
raise ValueError("auth_type can not be optional when auth_strategy is external")
|
|
46
|
+
|
|
47
|
+
if self.auth_type == AuthType.NOTSET:
|
|
48
|
+
self.auth_type = AuthType.REQUIRED if self.auth_strategy == AuthStrategy.EXTERNAL else AuthType.OPTIONAL
|
|
49
|
+
|
|
50
|
+
return self
|
|
18
51
|
|
|
19
52
|
@model_validator(mode="after")
|
|
20
53
|
def finalize_label(self) -> Self:
|
|
21
54
|
if self.label == "":
|
|
22
|
-
self.label = self.name
|
|
55
|
+
self.label = u.to_title_case(self.name)
|
|
23
56
|
return self
|
|
24
57
|
|
|
25
58
|
|
|
@@ -35,14 +68,11 @@ class PackageConfig(BaseModel):
|
|
|
35
68
|
return self
|
|
36
69
|
|
|
37
70
|
|
|
38
|
-
class _ConfigWithNameBaseModel(BaseModel):
|
|
39
|
-
name: str
|
|
40
|
-
|
|
41
|
-
|
|
42
71
|
class ConnectionTypeEnum(Enum):
|
|
43
72
|
SQLALCHEMY = "sqlalchemy"
|
|
44
73
|
CONNECTORX = "connectorx"
|
|
45
74
|
ADBC = "adbc"
|
|
75
|
+
DUCKDB = "duckdb"
|
|
46
76
|
|
|
47
77
|
|
|
48
78
|
class ConnectionProperties(BaseModel):
|
|
@@ -71,26 +101,32 @@ class ConnectionProperties(BaseModel):
|
|
|
71
101
|
|
|
72
102
|
@cached_property
|
|
73
103
|
def dialect(self) -> str:
|
|
104
|
+
default_dialect = None
|
|
74
105
|
if self.type == ConnectionTypeEnum.SQLALCHEMY:
|
|
75
106
|
dialect = self.engine.dialect.name
|
|
107
|
+
elif self.type == ConnectionTypeEnum.DUCKDB:
|
|
108
|
+
dialect = self.uri.split(':')[0]
|
|
109
|
+
default_dialect = 'duckdb'
|
|
76
110
|
else:
|
|
77
111
|
url = urlparse(self.uri)
|
|
78
112
|
dialect = url.scheme
|
|
79
113
|
|
|
80
|
-
processed_dialect = next((d for d in ['sqlite', 'postgres', 'mysql'] if dialect.lower().startswith(d)),
|
|
114
|
+
processed_dialect = next((d for d in ['sqlite', 'postgres', 'mysql', 'duckdb'] if dialect.lower().startswith(d)), default_dialect)
|
|
81
115
|
dialect = processed_dialect if processed_dialect is not None else dialect
|
|
82
116
|
return dialect
|
|
83
117
|
|
|
84
118
|
@cached_property
|
|
85
119
|
def attach_uri_for_duckdb(self) -> str | None:
|
|
86
|
-
if self.type == ConnectionTypeEnum.
|
|
120
|
+
if self.type == ConnectionTypeEnum.DUCKDB:
|
|
121
|
+
return self.uri
|
|
122
|
+
elif self.type == ConnectionTypeEnum.SQLALCHEMY:
|
|
87
123
|
url = self.engine.url
|
|
88
124
|
host = url.host
|
|
89
125
|
port = url.port
|
|
90
126
|
username = url.username
|
|
91
127
|
password = url.password
|
|
92
128
|
database = url.database
|
|
93
|
-
|
|
129
|
+
database_as_file = database if database is not None else ""
|
|
94
130
|
else:
|
|
95
131
|
url = urlparse(self.uri)
|
|
96
132
|
host = url.hostname
|
|
@@ -98,22 +134,36 @@ class ConnectionProperties(BaseModel):
|
|
|
98
134
|
username = url.username
|
|
99
135
|
password = url.password
|
|
100
136
|
database = url.path.lstrip('/')
|
|
101
|
-
|
|
137
|
+
database_as_file = self.uri.replace(f"{self.dialect}://", "")
|
|
102
138
|
|
|
103
|
-
if self.dialect
|
|
104
|
-
|
|
105
|
-
elif self.dialect
|
|
106
|
-
|
|
139
|
+
if self.dialect in ('postgres', 'mysql'):
|
|
140
|
+
attach_uri = f"{self.dialect}:dbname={database} user={username} password={password} host={host} port={port}"
|
|
141
|
+
elif self.dialect == "sqlite":
|
|
142
|
+
attach_uri = f"{self.dialect}:{database_as_file}"
|
|
143
|
+
elif self.dialect == "duckdb":
|
|
144
|
+
attach_uri = database_as_file
|
|
107
145
|
else:
|
|
108
|
-
|
|
146
|
+
attach_uri = None
|
|
147
|
+
|
|
148
|
+
return attach_uri
|
|
109
149
|
|
|
110
150
|
|
|
111
151
|
class DbConnConfig(ConnectionProperties, _ConfigWithNameBaseModel):
|
|
112
|
-
def finalize_uri(self,
|
|
113
|
-
self.uri = self.uri.format(project_path=
|
|
152
|
+
def finalize_uri(self, project_path: str) -> Self:
|
|
153
|
+
self.uri = self.uri.format(project_path=project_path)
|
|
114
154
|
return self
|
|
115
155
|
|
|
116
156
|
|
|
157
|
+
class ConfigurableOverride(BaseModel):
|
|
158
|
+
name: str
|
|
159
|
+
default: str
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class ConfigurablesConfig(ConfigurableOverride):
|
|
163
|
+
label: str = ""
|
|
164
|
+
description: str = ""
|
|
165
|
+
|
|
166
|
+
|
|
117
167
|
class ParametersConfig(BaseModel):
|
|
118
168
|
type: str
|
|
119
169
|
factory: str
|
|
@@ -129,8 +179,10 @@ class PermissionScope(Enum):
|
|
|
129
179
|
class AnalyticsOutputConfig(_ConfigWithNameBaseModel):
|
|
130
180
|
label: str = ""
|
|
131
181
|
description: str = ""
|
|
132
|
-
scope: PermissionScope =
|
|
182
|
+
scope: PermissionScope | None = None
|
|
133
183
|
parameters: list[str] | None = Field(default=None, description="The list of parameter names used by the dataset/dashboard")
|
|
184
|
+
configurables: list[ConfigurableOverride] = Field(default_factory=list)
|
|
185
|
+
project_configurables: dict[str, Any] | None = Field(default=None, exclude=True)
|
|
134
186
|
|
|
135
187
|
@model_validator(mode="after")
|
|
136
188
|
def finalize_label(self) -> Self:
|
|
@@ -140,7 +192,9 @@ class AnalyticsOutputConfig(_ConfigWithNameBaseModel):
|
|
|
140
192
|
|
|
141
193
|
@field_validator("scope", mode="before")
|
|
142
194
|
@classmethod
|
|
143
|
-
def validate_scope(cls, value:
|
|
195
|
+
def validate_scope(cls, value: Any, info: ValidationInfo) -> PermissionScope | None:
|
|
196
|
+
if value is None:
|
|
197
|
+
return None
|
|
144
198
|
try:
|
|
145
199
|
return PermissionScope[str(value).upper()]
|
|
146
200
|
except KeyError as e:
|
|
@@ -148,15 +202,23 @@ class AnalyticsOutputConfig(_ConfigWithNameBaseModel):
|
|
|
148
202
|
scope_list = [scope.name.lower() for scope in PermissionScope]
|
|
149
203
|
raise ValueError(f'Scope "{value}" is invalid for dataset/dashboard "{name}". Scope must be one of {scope_list}') from e
|
|
150
204
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
205
|
+
@model_validator(mode="after")
|
|
206
|
+
def validate_configurables(self) -> Self:
|
|
207
|
+
if self.project_configurables is not None:
|
|
208
|
+
for cfg_override in self.configurables:
|
|
209
|
+
if cfg_override.name not in self.project_configurables:
|
|
210
|
+
# Determine if it's a dataset or dashboard for better error message
|
|
211
|
+
class_name = self.__class__.__name__
|
|
212
|
+
type_str = "Dataset" if "Dataset" in class_name else "Dashboard" if "Dashboard" in class_name else "Asset"
|
|
213
|
+
raise ValueError(
|
|
214
|
+
f'{type_str} "{self.name}" references configurable "{cfg_override.name}" which is not defined '
|
|
215
|
+
f'in the project configurables'
|
|
216
|
+
)
|
|
217
|
+
return self
|
|
154
218
|
|
|
155
219
|
|
|
156
220
|
class DatasetConfig(AnalyticsOutputConfig):
|
|
157
221
|
model: str = ""
|
|
158
|
-
traits: dict = Field(default_factory=dict)
|
|
159
|
-
default_test_set: str = ""
|
|
160
222
|
|
|
161
223
|
def __hash__(self) -> int:
|
|
162
224
|
return hash("dataset_"+self.name)
|
|
@@ -168,14 +230,14 @@ class DatasetConfig(AnalyticsOutputConfig):
|
|
|
168
230
|
return self
|
|
169
231
|
|
|
170
232
|
|
|
233
|
+
class TestSetsUserConfig(BaseModel):
|
|
234
|
+
access_level: Literal["admin", "member", "guest"] = "guest"
|
|
235
|
+
custom_fields: dict[str, Any] = Field(default_factory=dict)
|
|
236
|
+
|
|
171
237
|
class TestSetsConfig(_ConfigWithNameBaseModel):
|
|
172
|
-
|
|
173
|
-
user_attributes: dict[str, Any] | None = None
|
|
238
|
+
user: TestSetsUserConfig = Field(default_factory=TestSetsUserConfig)
|
|
174
239
|
parameters: dict[str, Any] = Field(default_factory=dict)
|
|
175
|
-
|
|
176
|
-
@property
|
|
177
|
-
def is_authenticated(self) -> bool:
|
|
178
|
-
return self.user_attributes is not None
|
|
240
|
+
configurables: dict[str, Any] = Field(default_factory=dict)
|
|
179
241
|
|
|
180
242
|
|
|
181
243
|
class ManifestConfig(BaseModel):
|
|
@@ -183,11 +245,10 @@ class ManifestConfig(BaseModel):
|
|
|
183
245
|
packages: list[PackageConfig] = Field(default_factory=list)
|
|
184
246
|
connections: dict[str, DbConnConfig] = Field(default_factory=dict)
|
|
185
247
|
parameters: list[ParametersConfig] = Field(default_factory=list)
|
|
248
|
+
configurables: dict[str, ConfigurablesConfig] = Field(default_factory=dict)
|
|
186
249
|
selection_test_sets: dict[str, TestSetsConfig] = Field(default_factory=dict)
|
|
187
|
-
dataset_traits: dict[str, DatasetTraitConfig] = Field(default_factory=dict)
|
|
188
250
|
datasets: dict[str, DatasetConfig] = Field(default_factory=dict)
|
|
189
|
-
|
|
190
|
-
env_vars: dict[str, str] = Field(default_factory=dict)
|
|
251
|
+
project_path: str = "."
|
|
191
252
|
|
|
192
253
|
@field_validator("packages")
|
|
193
254
|
@classmethod
|
|
@@ -199,13 +260,13 @@ class ManifestConfig(BaseModel):
|
|
|
199
260
|
set_of_directories.add(package.directory)
|
|
200
261
|
return packages
|
|
201
262
|
|
|
202
|
-
@field_validator("connections", "selection_test_sets", "
|
|
263
|
+
@field_validator("connections", "selection_test_sets", "datasets", "configurables", mode="before")
|
|
203
264
|
@classmethod
|
|
204
265
|
def names_are_unique(cls, values: list[dict] | dict[str, dict], info: ValidationInfo) -> dict[str, dict]:
|
|
205
266
|
if isinstance(values, list):
|
|
206
267
|
values_as_dict = {}
|
|
207
268
|
for obj in values:
|
|
208
|
-
name = obj["name"]
|
|
269
|
+
name = u.normalize_name(obj["name"])
|
|
209
270
|
if name in values_as_dict:
|
|
210
271
|
raise ValueError(f'In the {info.field_name} section, the name "{name}" was specified multiple times')
|
|
211
272
|
values_as_dict[name] = obj
|
|
@@ -216,62 +277,79 @@ class ManifestConfig(BaseModel):
|
|
|
216
277
|
@model_validator(mode="after")
|
|
217
278
|
def finalize_connections(self) -> Self:
|
|
218
279
|
for conn in self.connections.values():
|
|
219
|
-
conn.finalize_uri(self.
|
|
280
|
+
conn.finalize_uri(self.project_path)
|
|
220
281
|
return self
|
|
221
282
|
|
|
222
283
|
@model_validator(mode="after")
|
|
223
|
-
def
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
284
|
+
def validate_authentication_and_scopes(self) -> Self:
|
|
285
|
+
"""
|
|
286
|
+
Enforce authentication rules:
|
|
287
|
+
- Set default scope based on auth_type if not specified.
|
|
288
|
+
- If auth_type is REQUIRED, no dataset may be PUBLIC.
|
|
289
|
+
"""
|
|
290
|
+
is_auth_required = self.project_variables.auth_type == AuthType.REQUIRED
|
|
291
|
+
default_scope = PermissionScope.PROTECTED if is_auth_required else PermissionScope.PUBLIC
|
|
292
|
+
|
|
293
|
+
for ds in self.datasets.values():
|
|
294
|
+
if ds.scope is None:
|
|
295
|
+
ds.scope = default_scope
|
|
296
|
+
|
|
297
|
+
if is_auth_required:
|
|
298
|
+
invalid = [name for name, ds in self.datasets.items() if ds.scope == PermissionScope.PUBLIC]
|
|
299
|
+
if invalid:
|
|
300
|
+
raise ValueError(
|
|
301
|
+
"Authentication is required, so datasets cannot be public.\n "
|
|
302
|
+
f"Update the scope for the following datasets: {invalid}\n "
|
|
303
|
+
)
|
|
238
304
|
return self
|
|
239
305
|
|
|
240
|
-
|
|
306
|
+
@model_validator(mode="after")
|
|
307
|
+
def validate_dataset_configurables(self) -> Self:
|
|
308
|
+
"""
|
|
309
|
+
Validate that dataset configurables reference valid project-level configurables.
|
|
310
|
+
"""
|
|
311
|
+
for dataset_cfg in self.datasets.values():
|
|
312
|
+
dataset_cfg.project_configurables = self.configurables
|
|
313
|
+
dataset_cfg.validate_configurables()
|
|
314
|
+
return self
|
|
315
|
+
|
|
316
|
+
def get_default_test_set(self) -> TestSetsConfig:
|
|
241
317
|
"""
|
|
242
318
|
Raises KeyError if dataset name doesn't exist
|
|
243
319
|
"""
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
default_name = default_name_1 if default_name_1 else default_name_2
|
|
247
|
-
default_test_set = self.selection_test_sets.get(default_name, TestSetsConfig(name=default_name))
|
|
320
|
+
default_default_test_set = TestSetsConfig(name=c.DEFAULT_TEST_SET_NAME)
|
|
321
|
+
default_test_set = self.selection_test_sets.get(c.DEFAULT_TEST_SET_NAME, default_default_test_set)
|
|
248
322
|
return default_test_set
|
|
249
323
|
|
|
250
|
-
def
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
324
|
+
def get_default_configurables(self, *, overrides: list[ConfigurableOverride] = []) -> dict[str, str]:
|
|
325
|
+
"""
|
|
326
|
+
Return a dictionary of configurable name to its default value.
|
|
327
|
+
|
|
328
|
+
Arguments:
|
|
329
|
+
overrides: A list of ConfigurableOverride objects to merge with the project-level defaults.
|
|
330
|
+
"""
|
|
331
|
+
defaults: dict[str, str] = {}
|
|
332
|
+
for name, cfg in self.configurables.items():
|
|
333
|
+
defaults[name] = str(cfg.default)
|
|
334
|
+
|
|
335
|
+
# Apply explicit overrides if provided
|
|
336
|
+
for cfg_override in overrides:
|
|
337
|
+
defaults[cfg_override.name] = cfg_override.default
|
|
338
|
+
|
|
339
|
+
return defaults
|
|
262
340
|
|
|
263
341
|
|
|
264
342
|
class ManifestIO:
|
|
265
|
-
|
|
266
343
|
@classmethod
|
|
267
|
-
def load_from_file(cls, logger: u.Logger,
|
|
344
|
+
def load_from_file(cls, logger: u.Logger, project_path: str, env_vars_unformatted: dict[str, str]) -> ManifestConfig:
|
|
268
345
|
start = time.time()
|
|
269
346
|
|
|
270
|
-
raw_content = u.read_file(u.Path(
|
|
271
|
-
content = u.render_string(raw_content,
|
|
272
|
-
manifest_content = yaml.safe_load(content)
|
|
347
|
+
raw_content = u.read_file(u.Path(project_path, c.MANIFEST_FILE))
|
|
348
|
+
content = u.render_string(raw_content, project_path=project_path, env_vars=env_vars_unformatted)
|
|
349
|
+
manifest_content: dict[str, Any] = yaml.safe_load(content)
|
|
350
|
+
|
|
273
351
|
try:
|
|
274
|
-
manifest_cfg = ManifestConfig(
|
|
352
|
+
manifest_cfg = ManifestConfig(project_path=project_path, **manifest_content)
|
|
275
353
|
except ValidationError as e:
|
|
276
354
|
raise u.ConfigurationError(f"Failed to process {c.MANIFEST_FILE} file. " + str(e)) from e
|
|
277
355
|
|