squirrels 0.1.0__py3-none-any.whl → 0.6.0.post0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dateutils/__init__.py +6 -0
- dateutils/_enums.py +25 -0
- squirrels/dateutils.py → dateutils/_implementation.py +409 -380
- dateutils/types.py +6 -0
- squirrels/__init__.py +21 -18
- squirrels/_api_routes/__init__.py +5 -0
- squirrels/_api_routes/auth.py +337 -0
- squirrels/_api_routes/base.py +196 -0
- squirrels/_api_routes/dashboards.py +156 -0
- squirrels/_api_routes/data_management.py +148 -0
- squirrels/_api_routes/datasets.py +220 -0
- squirrels/_api_routes/project.py +289 -0
- squirrels/_api_server.py +552 -134
- squirrels/_arguments/__init__.py +0 -0
- squirrels/_arguments/init_time_args.py +83 -0
- squirrels/_arguments/run_time_args.py +111 -0
- squirrels/_auth.py +777 -0
- squirrels/_command_line.py +239 -107
- squirrels/_compile_prompts.py +147 -0
- squirrels/_connection_set.py +94 -0
- squirrels/_constants.py +141 -64
- squirrels/_dashboards.py +179 -0
- squirrels/_data_sources.py +570 -0
- squirrels/_dataset_types.py +91 -0
- squirrels/_env_vars.py +209 -0
- squirrels/_exceptions.py +29 -0
- squirrels/_http_error_responses.py +52 -0
- squirrels/_initializer.py +319 -110
- squirrels/_logging.py +121 -0
- squirrels/_manifest.py +357 -187
- squirrels/_mcp_server.py +578 -0
- squirrels/_model_builder.py +69 -0
- squirrels/_model_configs.py +74 -0
- squirrels/_model_queries.py +52 -0
- squirrels/_models.py +1201 -0
- squirrels/_package_data/base_project/.env +7 -0
- squirrels/_package_data/base_project/.env.example +44 -0
- squirrels/_package_data/base_project/connections.yml +16 -0
- squirrels/_package_data/base_project/dashboards/dashboard_example.py +40 -0
- squirrels/_package_data/base_project/dashboards/dashboard_example.yml +22 -0
- squirrels/_package_data/base_project/docker/.dockerignore +16 -0
- squirrels/_package_data/base_project/docker/Dockerfile +16 -0
- squirrels/_package_data/base_project/docker/compose.yml +7 -0
- squirrels/_package_data/base_project/duckdb_init.sql +10 -0
- squirrels/_package_data/base_project/gitignore +13 -0
- squirrels/_package_data/base_project/macros/macros_example.sql +17 -0
- squirrels/_package_data/base_project/models/builds/build_example.py +26 -0
- squirrels/_package_data/base_project/models/builds/build_example.sql +16 -0
- squirrels/_package_data/base_project/models/builds/build_example.yml +57 -0
- squirrels/_package_data/base_project/models/dbviews/dbview_example.sql +17 -0
- squirrels/_package_data/base_project/models/dbviews/dbview_example.yml +32 -0
- squirrels/_package_data/base_project/models/federates/federate_example.py +51 -0
- squirrels/_package_data/base_project/models/federates/federate_example.sql +21 -0
- squirrels/_package_data/base_project/models/federates/federate_example.yml +65 -0
- squirrels/_package_data/base_project/models/sources.yml +38 -0
- squirrels/_package_data/base_project/parameters.yml +142 -0
- squirrels/_package_data/base_project/pyconfigs/connections.py +19 -0
- squirrels/_package_data/base_project/pyconfigs/context.py +96 -0
- squirrels/_package_data/base_project/pyconfigs/parameters.py +141 -0
- squirrels/_package_data/base_project/pyconfigs/user.py +56 -0
- squirrels/_package_data/base_project/resources/expenses.db +0 -0
- squirrels/_package_data/base_project/resources/public/.gitkeep +0 -0
- squirrels/_package_data/base_project/resources/weather.db +0 -0
- squirrels/_package_data/base_project/seeds/seed_categories.csv +6 -0
- squirrels/_package_data/base_project/seeds/seed_categories.yml +15 -0
- squirrels/_package_data/base_project/seeds/seed_subcategories.csv +15 -0
- squirrels/_package_data/base_project/seeds/seed_subcategories.yml +21 -0
- squirrels/_package_data/base_project/squirrels.yml.j2 +61 -0
- squirrels/_package_data/base_project/tmp/.gitignore +2 -0
- squirrels/_package_data/templates/login_successful.html +53 -0
- squirrels/_package_data/templates/squirrels_studio.html +22 -0
- squirrels/_package_loader.py +29 -0
- squirrels/_parameter_configs.py +592 -0
- squirrels/_parameter_options.py +348 -0
- squirrels/_parameter_sets.py +207 -0
- squirrels/_parameters.py +1703 -0
- squirrels/_project.py +796 -0
- squirrels/_py_module.py +122 -0
- squirrels/_request_context.py +33 -0
- squirrels/_schemas/__init__.py +0 -0
- squirrels/_schemas/auth_models.py +83 -0
- squirrels/_schemas/query_param_models.py +70 -0
- squirrels/_schemas/request_models.py +26 -0
- squirrels/_schemas/response_models.py +286 -0
- squirrels/_seeds.py +97 -0
- squirrels/_sources.py +112 -0
- squirrels/_utils.py +540 -149
- squirrels/_version.py +1 -3
- squirrels/arguments.py +7 -0
- squirrels/auth.py +4 -0
- squirrels/connections.py +3 -0
- squirrels/dashboards.py +3 -0
- squirrels/data_sources.py +14 -282
- squirrels/parameter_options.py +13 -189
- squirrels/parameters.py +14 -801
- squirrels/types.py +18 -0
- squirrels-0.6.0.post0.dist-info/METADATA +148 -0
- squirrels-0.6.0.post0.dist-info/RECORD +101 -0
- {squirrels-0.1.0.dist-info → squirrels-0.6.0.post0.dist-info}/WHEEL +1 -2
- {squirrels-0.1.0.dist-info → squirrels-0.6.0.post0.dist-info}/entry_points.txt +1 -0
- squirrels-0.6.0.post0.dist-info/licenses/LICENSE +201 -0
- squirrels/_credentials_manager.py +0 -87
- squirrels/_module_loader.py +0 -37
- squirrels/_parameter_set.py +0 -151
- squirrels/_renderer.py +0 -286
- squirrels/_timed_imports.py +0 -37
- squirrels/connection_set.py +0 -126
- squirrels/package_data/base_project/.gitignore +0 -4
- squirrels/package_data/base_project/connections.py +0 -21
- squirrels/package_data/base_project/database/sample_database.db +0 -0
- squirrels/package_data/base_project/database/seattle_weather.db +0 -0
- squirrels/package_data/base_project/datasets/sample_dataset/context.py +0 -8
- squirrels/package_data/base_project/datasets/sample_dataset/database_view1.py +0 -23
- squirrels/package_data/base_project/datasets/sample_dataset/database_view1.sql.j2 +0 -7
- squirrels/package_data/base_project/datasets/sample_dataset/final_view.py +0 -10
- squirrels/package_data/base_project/datasets/sample_dataset/final_view.sql.j2 +0 -2
- squirrels/package_data/base_project/datasets/sample_dataset/parameters.py +0 -30
- squirrels/package_data/base_project/datasets/sample_dataset/selections.cfg +0 -6
- squirrels/package_data/base_project/squirrels.yaml +0 -26
- squirrels/package_data/static/favicon.ico +0 -0
- squirrels/package_data/static/script.js +0 -234
- squirrels/package_data/static/style.css +0 -110
- squirrels/package_data/templates/index.html +0 -32
- squirrels-0.1.0.dist-info/LICENSE +0 -22
- squirrels-0.1.0.dist-info/METADATA +0 -67
- squirrels-0.1.0.dist-info/RECORD +0 -40
- squirrels-0.1.0.dist-info/top_level.txt +0 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import shutil, os, time
|
|
2
|
+
|
|
3
|
+
from . import _constants as c, _utils as u
|
|
4
|
+
from ._manifest import ManifestConfig
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PackageLoaderIO:
|
|
8
|
+
|
|
9
|
+
@classmethod
|
|
10
|
+
def load_packages(cls, logger: u.Logger, manifest_cfg: ManifestConfig, *, reload: bool = False) -> None:
|
|
11
|
+
start = time.time()
|
|
12
|
+
|
|
13
|
+
# Importing git here avoids requirement of having git installed on system if not needed
|
|
14
|
+
import git
|
|
15
|
+
|
|
16
|
+
# If reload, delete the modules directory (if it exists). It will be recreated later
|
|
17
|
+
if reload and os.path.exists(c.PACKAGES_FOLDER):
|
|
18
|
+
shutil.rmtree(c.PACKAGES_FOLDER)
|
|
19
|
+
|
|
20
|
+
package_repos = manifest_cfg.packages
|
|
21
|
+
for repo in package_repos:
|
|
22
|
+
target_dir = f"{c.PACKAGES_FOLDER}/{repo.directory}"
|
|
23
|
+
if not os.path.exists(target_dir):
|
|
24
|
+
try:
|
|
25
|
+
git.Repo.clone_from(repo.git, target_dir, branch=repo.revision, depth=1)
|
|
26
|
+
except git.GitCommandError as e:
|
|
27
|
+
raise u.ConfigurationError(f"Git clone of package failed for this repository: {repo.git}") from e
|
|
28
|
+
|
|
29
|
+
logger.log_activity_time("loading packages", start)
|
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Generic, TypeVar, Annotated, Sequence, Iterator, Any
|
|
3
|
+
from typing_extensions import Self
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from abc import ABCMeta, abstractmethod
|
|
7
|
+
from copy import copy
|
|
8
|
+
from fastapi import Query
|
|
9
|
+
from pydantic.fields import Field
|
|
10
|
+
import polars as pl, re
|
|
11
|
+
|
|
12
|
+
from . import _data_sources as d, _parameter_options as po, _parameters as p, _utils as u, _constants as c
|
|
13
|
+
from ._exceptions import InvalidInputError
|
|
14
|
+
from ._schemas.auth_models import AbstractUser
|
|
15
|
+
from ._connection_set import ConnectionSet
|
|
16
|
+
from ._seeds import Seeds
|
|
17
|
+
|
|
18
|
+
ParamOptionType = TypeVar("ParamOptionType", bound=po.ParameterOption)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class APIParamFieldInfo:
|
|
23
|
+
name: str
|
|
24
|
+
type: type
|
|
25
|
+
title: str = ""
|
|
26
|
+
description: str = ""
|
|
27
|
+
examples: list[Any] | None = None
|
|
28
|
+
pattern: str | None = None
|
|
29
|
+
min_length: int | None = None
|
|
30
|
+
max_length: int | None = None
|
|
31
|
+
default: Any = None
|
|
32
|
+
|
|
33
|
+
def as_query_info(self):
|
|
34
|
+
query_info = Query(
|
|
35
|
+
title=self.title, description=self.description, examples=self.examples, pattern=self.pattern,
|
|
36
|
+
min_length=self.min_length, max_length=self.max_length
|
|
37
|
+
)
|
|
38
|
+
return (self.name, Annotated[self.type, query_info], self.default)
|
|
39
|
+
|
|
40
|
+
def as_body_info(self):
|
|
41
|
+
field_info = Field(self.default,
|
|
42
|
+
title=self.title, description=self.description, examples=self.examples, pattern=self.pattern,
|
|
43
|
+
min_length=self.min_length, max_length=self.max_length
|
|
44
|
+
)
|
|
45
|
+
return (self.type, field_info)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class ParameterConfigBase(metaclass=ABCMeta):
|
|
50
|
+
"""
|
|
51
|
+
Abstract class for all parameter classes
|
|
52
|
+
"""
|
|
53
|
+
name: str
|
|
54
|
+
label: str
|
|
55
|
+
description: str = field(default="", kw_only=True)
|
|
56
|
+
user_attribute: str | None = field(default=None, kw_only=True)
|
|
57
|
+
parent_name: str | None = field(default=None, kw_only=True)
|
|
58
|
+
|
|
59
|
+
def _get_user_group(self, user: AbstractUser) -> Any:
|
|
60
|
+
if self.user_attribute is None:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
final_object = user
|
|
64
|
+
attribute = self.user_attribute
|
|
65
|
+
try:
|
|
66
|
+
if "." in attribute:
|
|
67
|
+
parts = attribute.split(".", 1)
|
|
68
|
+
final_object = getattr(final_object, parts[0])
|
|
69
|
+
attribute = parts[1]
|
|
70
|
+
return getattr(final_object, attribute)
|
|
71
|
+
except AttributeError:
|
|
72
|
+
raise u.ConfigurationError(f"User attribute '{self.user_attribute}' is not valid")
|
|
73
|
+
|
|
74
|
+
def copy(self):
|
|
75
|
+
"""
|
|
76
|
+
Use for unit testing only
|
|
77
|
+
"""
|
|
78
|
+
return copy(self)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class ParameterConfig(Generic[ParamOptionType], ParameterConfigBase):
|
|
83
|
+
"""
|
|
84
|
+
Abstract class for all parameter classes (except DataSourceParameters)
|
|
85
|
+
"""
|
|
86
|
+
_all_options: Sequence[ParamOptionType] = field(repr=False)
|
|
87
|
+
|
|
88
|
+
@abstractmethod
|
|
89
|
+
def __init__(
|
|
90
|
+
self, name: str, label: str, all_options: Sequence[ParamOptionType | dict], *, description: str = "",
|
|
91
|
+
user_attribute: str | None = None, parent_name: str | None = None
|
|
92
|
+
) -> None:
|
|
93
|
+
super().__init__(name, label, description=description, user_attribute=user_attribute, parent_name=parent_name)
|
|
94
|
+
self._all_options = tuple(self._to_param_option(x) for x in all_options)
|
|
95
|
+
|
|
96
|
+
def _to_param_option(self, option: ParamOptionType | dict) -> ParamOptionType:
|
|
97
|
+
return self.ParameterOption(**option) if isinstance(option, dict) else option
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def all_options(self) -> Sequence[ParamOptionType]:
|
|
101
|
+
return self._all_options
|
|
102
|
+
|
|
103
|
+
@staticmethod
|
|
104
|
+
@abstractmethod
|
|
105
|
+
def widget_type() -> str:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
@abstractmethod
|
|
110
|
+
def ParameterOption(*args, **kwargs) -> ParamOptionType:
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
@staticmethod
|
|
114
|
+
@abstractmethod
|
|
115
|
+
def DataSource(*args, **kwargs) -> d.DataSource:
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
def _invalid_input_error(self, selection: str, more_details: str = '') -> InvalidInputError:
|
|
119
|
+
return InvalidInputError(400, "invalid_parameter_selection", f'Selected value "{selection}" is not valid for parameter "{self.name}". ' + more_details)
|
|
120
|
+
|
|
121
|
+
@abstractmethod
|
|
122
|
+
def with_selection(
|
|
123
|
+
self, selection: str | None, user: AbstractUser, parent_param: p._SelectionParameter | None
|
|
124
|
+
) -> p.Parameter:
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
def _get_options_iterator(
|
|
128
|
+
self, all_options: Sequence[ParamOptionType], user: AbstractUser, parent_param: p._SelectionParameter | None
|
|
129
|
+
) -> Iterator[ParamOptionType]:
|
|
130
|
+
user_group = self._get_user_group(user)
|
|
131
|
+
selected_parent_option_ids = frozenset(parent_param._get_selected_ids_as_list()) if parent_param else None
|
|
132
|
+
return (x for x in all_options if x._is_valid(user_group, selected_parent_option_ids))
|
|
133
|
+
|
|
134
|
+
@abstractmethod
|
|
135
|
+
def get_api_field_info(self) -> APIParamFieldInfo:
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass
|
|
140
|
+
class SelectionParameterConfig(ParameterConfig[po.SelectParameterOption]):
|
|
141
|
+
"""
|
|
142
|
+
Abstract class for select parameter classes (single-select, multi-select, etc)
|
|
143
|
+
"""
|
|
144
|
+
children: dict[str, ParameterConfigBase] = field(default_factory=dict, init=False, repr=False)
|
|
145
|
+
trigger_refresh: bool = field(default=False, init=False)
|
|
146
|
+
|
|
147
|
+
@abstractmethod
|
|
148
|
+
def __init__(
|
|
149
|
+
self, name: str, label: str, all_options: Sequence[po.SelectParameterOption | dict], *,
|
|
150
|
+
description: str = "", user_attribute: str | None = None, parent_name: str | None = None
|
|
151
|
+
) -> None:
|
|
152
|
+
super().__init__(name, label, all_options, description=description, user_attribute=user_attribute, parent_name=parent_name)
|
|
153
|
+
self.children: dict[str, ParameterConfigBase] = dict()
|
|
154
|
+
self.trigger_refresh = False
|
|
155
|
+
|
|
156
|
+
@staticmethod
|
|
157
|
+
def ParameterOption(*args, **kwargs):
|
|
158
|
+
return po.SelectParameterOption(*args, **kwargs)
|
|
159
|
+
|
|
160
|
+
def _add_child_mutate(self, child: ParameterConfigBase):
|
|
161
|
+
self.children[child.name] = child
|
|
162
|
+
self.trigger_refresh = True
|
|
163
|
+
|
|
164
|
+
def _get_options(self, user: AbstractUser, parent_param: p._SelectionParameter | None) -> Sequence[po.SelectParameterOption]:
|
|
165
|
+
return tuple(self._get_options_iterator(self.all_options, user, parent_param))
|
|
166
|
+
|
|
167
|
+
def _get_default_ids_iterator(self, options: Sequence[po.SelectParameterOption]) -> Iterator[str]:
|
|
168
|
+
return (x._identifier for x in options if x._is_default)
|
|
169
|
+
|
|
170
|
+
def copy(self) -> Self:
|
|
171
|
+
"""
|
|
172
|
+
Use for unit testing only
|
|
173
|
+
"""
|
|
174
|
+
other = super().copy()
|
|
175
|
+
other.children = self.children.copy()
|
|
176
|
+
return other
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@dataclass
|
|
180
|
+
class SingleSelectParameterConfig(SelectionParameterConfig):
|
|
181
|
+
"""
|
|
182
|
+
Class to define configurations for single-select parameter widgets.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
def __init__(
|
|
186
|
+
self, name: str, label: str, all_options: Sequence[po.SelectParameterOption | dict], *, description: str = "",
|
|
187
|
+
user_attribute: str | None = None, parent_name: str | None = None
|
|
188
|
+
) -> None:
|
|
189
|
+
super().__init__(name, label, all_options, description=description, user_attribute=user_attribute, parent_name=parent_name)
|
|
190
|
+
|
|
191
|
+
@staticmethod
|
|
192
|
+
def widget_type() -> str:
|
|
193
|
+
return "single_select"
|
|
194
|
+
|
|
195
|
+
@staticmethod
|
|
196
|
+
def DataSource(*args, **kwargs):
|
|
197
|
+
return d.SelectDataSource(*args, **kwargs)
|
|
198
|
+
|
|
199
|
+
def with_selection(
|
|
200
|
+
self, selection: str | None, user: AbstractUser, parent_param: p._SelectionParameter | None
|
|
201
|
+
) -> p.SingleSelectParameter:
|
|
202
|
+
options = self._get_options(user, parent_param)
|
|
203
|
+
if selection is None:
|
|
204
|
+
selected_id = next(self._get_default_ids_iterator(options), None)
|
|
205
|
+
if selected_id is None and len(options) > 0:
|
|
206
|
+
selected_id = options[0]._identifier
|
|
207
|
+
else:
|
|
208
|
+
selected_id = selection
|
|
209
|
+
return p.SingleSelectParameter(self, options, selected_id)
|
|
210
|
+
|
|
211
|
+
def get_api_field_info(self) -> APIParamFieldInfo:
|
|
212
|
+
examples = [x._identifier for x in self.all_options]
|
|
213
|
+
return APIParamFieldInfo(
|
|
214
|
+
self.name, str, title=self.label, description=self.description, examples=examples
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@dataclass
|
|
219
|
+
class MultiSelectParameterConfig(SelectionParameterConfig):
|
|
220
|
+
"""
|
|
221
|
+
Class to define configurations for multi-select parameter widgets.
|
|
222
|
+
"""
|
|
223
|
+
show_select_all: bool = field(default=True, kw_only=True)
|
|
224
|
+
order_matters: bool = field(default=False, kw_only=True)
|
|
225
|
+
none_is_all: bool = field(default=True, kw_only=True)
|
|
226
|
+
|
|
227
|
+
def __init__(
|
|
228
|
+
self, name: str, label: str, all_options: Sequence[po.SelectParameterOption | dict], *, description: str = "",
|
|
229
|
+
show_select_all: bool = True, order_matters: bool = False, none_is_all: bool = True,
|
|
230
|
+
user_attribute: str | None = None, parent_name: str | None = None
|
|
231
|
+
) -> None:
|
|
232
|
+
super().__init__(
|
|
233
|
+
name, label, all_options, description=description, user_attribute=user_attribute, parent_name=parent_name
|
|
234
|
+
)
|
|
235
|
+
self.show_select_all = show_select_all
|
|
236
|
+
self.order_matters = order_matters
|
|
237
|
+
self.none_is_all = none_is_all
|
|
238
|
+
|
|
239
|
+
@staticmethod
|
|
240
|
+
def widget_type() -> str:
|
|
241
|
+
return "multi_select"
|
|
242
|
+
|
|
243
|
+
@staticmethod
|
|
244
|
+
def DataSource(*args, **kwargs):
|
|
245
|
+
return d.SelectDataSource(*args, **kwargs)
|
|
246
|
+
|
|
247
|
+
def with_selection(
|
|
248
|
+
self, selection: str | None, user: AbstractUser, parent_param: p._SelectionParameter | None
|
|
249
|
+
) -> p.MultiSelectParameter:
|
|
250
|
+
options = self._get_options(user, parent_param)
|
|
251
|
+
if selection is None:
|
|
252
|
+
selected_ids = tuple(self._get_default_ids_iterator(options))
|
|
253
|
+
else:
|
|
254
|
+
selected_ids = u.load_json_or_comma_delimited_str_as_list(selection)
|
|
255
|
+
return p.MultiSelectParameter(self, options, selected_ids)
|
|
256
|
+
|
|
257
|
+
def get_api_field_info(self) -> APIParamFieldInfo:
|
|
258
|
+
identifiers = [x._identifier for x in self.all_options]
|
|
259
|
+
return APIParamFieldInfo(
|
|
260
|
+
self.name, list[str], title=self.label, description=self.description, examples=[identifiers]
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@dataclass
|
|
265
|
+
class _DateTypeParameterConfig(ParameterConfig[ParamOptionType]):
|
|
266
|
+
"""
|
|
267
|
+
Abstract class for date and date range parameter configs
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
@abstractmethod
|
|
271
|
+
def __init__(
|
|
272
|
+
self, name: str, label: str, all_options: Sequence[ParamOptionType | dict], *, description: str = "",
|
|
273
|
+
user_attribute: str | None = None, parent_name: str | None = None
|
|
274
|
+
) -> None:
|
|
275
|
+
super().__init__(name, label, all_options, description=description, user_attribute=user_attribute, parent_name=parent_name)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@dataclass
|
|
279
|
+
class DateParameterConfig(_DateTypeParameterConfig[po.DateParameterOption]):
|
|
280
|
+
"""
|
|
281
|
+
Class to define configurations for date parameter widgets.
|
|
282
|
+
"""
|
|
283
|
+
_all_options: Sequence[po.DateParameterOption] = field(repr=False)
|
|
284
|
+
|
|
285
|
+
def __init__(
|
|
286
|
+
self, name: str, label: str, all_options: Sequence[po.DateParameterOption | dict], *,
|
|
287
|
+
description: str = "", user_attribute: str | None = None, parent_name: str | None = None
|
|
288
|
+
) -> None:
|
|
289
|
+
super().__init__(name, label, all_options, description=description, user_attribute=user_attribute, parent_name=parent_name)
|
|
290
|
+
|
|
291
|
+
@staticmethod
|
|
292
|
+
def widget_type() -> str:
|
|
293
|
+
return "date"
|
|
294
|
+
|
|
295
|
+
@staticmethod
|
|
296
|
+
def ParameterOption(*args, **kwargs):
|
|
297
|
+
return po.DateParameterOption(*args, **kwargs)
|
|
298
|
+
|
|
299
|
+
@staticmethod
|
|
300
|
+
def DataSource(*args, **kwargs):
|
|
301
|
+
return d.DateDataSource(*args, **kwargs)
|
|
302
|
+
|
|
303
|
+
def with_selection(
|
|
304
|
+
self, selection: str | None, user: AbstractUser, parent_param: p._SelectionParameter | None
|
|
305
|
+
) -> p.DateParameter:
|
|
306
|
+
curr_option: po.DateParameterOption | None = next(self._get_options_iterator(self.all_options, user, parent_param), None)
|
|
307
|
+
selected_date = curr_option._default_date if selection is None and curr_option is not None else selection
|
|
308
|
+
return p.DateParameter(self, curr_option, selected_date)
|
|
309
|
+
|
|
310
|
+
def get_api_field_info(self) -> APIParamFieldInfo:
|
|
311
|
+
examples = [str(x._default_date) for x in self.all_options]
|
|
312
|
+
return APIParamFieldInfo(
|
|
313
|
+
self.name, str, title=self.label, description=self.description, examples=examples, pattern=c.DATE_REGEX
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
@dataclass
|
|
318
|
+
class DateRangeParameterConfig(_DateTypeParameterConfig[po.DateRangeParameterOption]):
|
|
319
|
+
"""
|
|
320
|
+
Class to define configurations for date range parameter widgets.
|
|
321
|
+
"""
|
|
322
|
+
_all_options: Sequence[po.DateRangeParameterOption] = field(repr=False)
|
|
323
|
+
|
|
324
|
+
def __init__(
|
|
325
|
+
self, name: str, label: str, all_options: Sequence[po.DateRangeParameterOption | dict], *,
|
|
326
|
+
description: str = "", user_attribute: str | None = None, parent_name: str | None = None
|
|
327
|
+
) -> None:
|
|
328
|
+
super().__init__(name, label, all_options, description=description, user_attribute=user_attribute, parent_name=parent_name)
|
|
329
|
+
|
|
330
|
+
@staticmethod
|
|
331
|
+
def widget_type() -> str:
|
|
332
|
+
return "date_range"
|
|
333
|
+
|
|
334
|
+
@staticmethod
|
|
335
|
+
def ParameterOption(*args, **kwargs):
|
|
336
|
+
return po.DateRangeParameterOption(*args, **kwargs)
|
|
337
|
+
|
|
338
|
+
@staticmethod
|
|
339
|
+
def DataSource(*args, **kwargs):
|
|
340
|
+
return d.DateRangeDataSource(*args, **kwargs)
|
|
341
|
+
|
|
342
|
+
def with_selection(
|
|
343
|
+
self, selection: str | None, user: AbstractUser, parent_param: p._SelectionParameter | None
|
|
344
|
+
) -> p.DateRangeParameter:
|
|
345
|
+
curr_option: po.DateRangeParameterOption | None = next(self._get_options_iterator(self.all_options, user, parent_param), None)
|
|
346
|
+
if selection is None:
|
|
347
|
+
if curr_option is not None:
|
|
348
|
+
selected_start_date = curr_option._default_start_date
|
|
349
|
+
selected_end_date = curr_option._default_end_date
|
|
350
|
+
else:
|
|
351
|
+
selected_start_date, selected_end_date = None, None
|
|
352
|
+
else:
|
|
353
|
+
try:
|
|
354
|
+
selected_start_date, selected_end_date = u.load_json_or_comma_delimited_str_as_list(selection)
|
|
355
|
+
except ValueError:
|
|
356
|
+
raise self._invalid_input_error(selection, "Date range parameter selection must be two dates.")
|
|
357
|
+
return p.DateRangeParameter(self, curr_option, selected_start_date, selected_end_date)
|
|
358
|
+
|
|
359
|
+
def get_api_field_info(self) -> APIParamFieldInfo:
|
|
360
|
+
examples = [[str(x._default_start_date), str(x._default_end_date)] for x in self.all_options]
|
|
361
|
+
return APIParamFieldInfo(
|
|
362
|
+
self.name, list[str], title=self.label, description=self.description, examples=examples, max_length=2
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
@dataclass
|
|
367
|
+
class _NumericParameterConfig(ParameterConfig[ParamOptionType]):
|
|
368
|
+
"""
|
|
369
|
+
Abstract class for number and number range parameter configs
|
|
370
|
+
"""
|
|
371
|
+
|
|
372
|
+
@abstractmethod
|
|
373
|
+
def __init__(
|
|
374
|
+
self, name: str, label: str, all_options: Sequence[ParamOptionType | dict], *, description: str = "",
|
|
375
|
+
user_attribute: str | None = None, parent_name: str | None = None
|
|
376
|
+
) -> None:
|
|
377
|
+
super().__init__(name, label, all_options, description=description, user_attribute=user_attribute, parent_name=parent_name)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
@dataclass
|
|
381
|
+
class NumberParameterConfig(_NumericParameterConfig[po.NumberParameterOption]):
|
|
382
|
+
"""
|
|
383
|
+
Class to define configurations for number parameter widgets.
|
|
384
|
+
"""
|
|
385
|
+
_all_options: Sequence[po.NumberParameterOption] = field(repr=False)
|
|
386
|
+
|
|
387
|
+
def __init__(
|
|
388
|
+
self, name: str, label: str, all_options: Sequence[po.NumberParameterOption | dict], *,
|
|
389
|
+
description: str = "", user_attribute: str | None = None, parent_name: str | None = None
|
|
390
|
+
) -> None:
|
|
391
|
+
super().__init__(name, label, all_options, description=description, user_attribute=user_attribute, parent_name=parent_name)
|
|
392
|
+
|
|
393
|
+
@staticmethod
|
|
394
|
+
def widget_type() -> str:
|
|
395
|
+
return "number"
|
|
396
|
+
|
|
397
|
+
@staticmethod
|
|
398
|
+
def ParameterOption(*args, **kwargs):
|
|
399
|
+
return po.NumberParameterOption(*args, **kwargs)
|
|
400
|
+
|
|
401
|
+
@staticmethod
|
|
402
|
+
def DataSource(*args, **kwargs):
|
|
403
|
+
return d.NumberDataSource(*args, **kwargs)
|
|
404
|
+
|
|
405
|
+
def with_selection(
|
|
406
|
+
self, selection: str | None, user: AbstractUser, parent_param: p._SelectionParameter | None
|
|
407
|
+
) -> p.NumberParameter:
|
|
408
|
+
curr_option: po.NumberParameterOption | None = next(self._get_options_iterator(self.all_options, user, parent_param), None)
|
|
409
|
+
selected_value = curr_option._default_value if selection is None and curr_option is not None else selection
|
|
410
|
+
return p.NumberParameter(self, curr_option, selected_value)
|
|
411
|
+
|
|
412
|
+
def get_api_field_info(self) -> APIParamFieldInfo:
|
|
413
|
+
examples = [x._default_value for x in self.all_options]
|
|
414
|
+
return APIParamFieldInfo(
|
|
415
|
+
self.name, float, title=self.label, description=self.description, examples=examples
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@dataclass
|
|
420
|
+
class NumberRangeParameterConfig(_NumericParameterConfig[po.NumberRangeParameterOption]):
|
|
421
|
+
"""
|
|
422
|
+
Class to define configurations for number range parameter widgets.
|
|
423
|
+
"""
|
|
424
|
+
_all_options: Sequence[po.NumberRangeParameterOption] = field(repr=False)
|
|
425
|
+
|
|
426
|
+
def __init__(
|
|
427
|
+
self, name: str, label: str, all_options: Sequence[po.NumberRangeParameterOption | dict], *,
|
|
428
|
+
description: str = "", user_attribute: str | None = None, parent_name: str | None = None
|
|
429
|
+
) -> None:
|
|
430
|
+
super().__init__(name, label, all_options, description=description, user_attribute=user_attribute, parent_name=parent_name)
|
|
431
|
+
|
|
432
|
+
@staticmethod
|
|
433
|
+
def widget_type() -> str:
|
|
434
|
+
return "number_range"
|
|
435
|
+
|
|
436
|
+
@staticmethod
|
|
437
|
+
def ParameterOption(*args, **kwargs):
|
|
438
|
+
return po.NumberRangeParameterOption(*args, **kwargs)
|
|
439
|
+
|
|
440
|
+
@staticmethod
|
|
441
|
+
def DataSource(*args, **kwargs):
|
|
442
|
+
return d.NumberRangeDataSource(*args, **kwargs)
|
|
443
|
+
|
|
444
|
+
def with_selection(
|
|
445
|
+
self, selection: str | None, user: AbstractUser, parent_param: p._SelectionParameter | None
|
|
446
|
+
) -> p.NumberRangeParameter:
|
|
447
|
+
curr_option: po.NumberRangeParameterOption | None = next(self._get_options_iterator(self.all_options, user, parent_param), None)
|
|
448
|
+
if selection is None:
|
|
449
|
+
if curr_option is not None:
|
|
450
|
+
selected_lower_value = curr_option._default_lower_value
|
|
451
|
+
selected_upper_value = curr_option._default_upper_value
|
|
452
|
+
else:
|
|
453
|
+
selected_lower_value, selected_upper_value = None, None
|
|
454
|
+
else:
|
|
455
|
+
try:
|
|
456
|
+
selected_lower_value, selected_upper_value = u.load_json_or_comma_delimited_str_as_list(selection)
|
|
457
|
+
except ValueError:
|
|
458
|
+
raise self._invalid_input_error(selection, "Number range parameter selection must be two numbers.")
|
|
459
|
+
return p.NumberRangeParameter(self, curr_option, selected_lower_value, selected_upper_value)
|
|
460
|
+
|
|
461
|
+
def get_api_field_info(self) -> APIParamFieldInfo:
|
|
462
|
+
examples = [[x._default_lower_value, x._default_upper_value] for x in self.all_options]
|
|
463
|
+
return APIParamFieldInfo(
|
|
464
|
+
self.name, list[str], title=self.label, description=self.description, examples=examples, max_length=2
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
@dataclass
|
|
469
|
+
class TextParameterConfig(ParameterConfig[po.TextParameterOption]):
|
|
470
|
+
"""
|
|
471
|
+
Class to define configurations for text parameter widgets.
|
|
472
|
+
"""
|
|
473
|
+
_all_options: Sequence[po.TextParameterOption] = field(repr=False)
|
|
474
|
+
input_type: str
|
|
475
|
+
|
|
476
|
+
def __init__(
|
|
477
|
+
self, name: str, label: str, all_options: Sequence[po.TextParameterOption | dict], *, description: str = "",
|
|
478
|
+
input_type: str = "text", user_attribute: str | None = None, parent_name: str | None = None
|
|
479
|
+
) -> None:
|
|
480
|
+
super().__init__(name, label, all_options, description=description, user_attribute=user_attribute, parent_name=parent_name)
|
|
481
|
+
|
|
482
|
+
allowed_input_types = ["text", "textarea", "number", "date", "datetime-local", "month", "time", "color", "password"]
|
|
483
|
+
if input_type not in allowed_input_types:
|
|
484
|
+
raise u.ConfigurationError(f"Invalid input type '{input_type}' for text parameter '{name}'. Must be one of {allowed_input_types}.")
|
|
485
|
+
|
|
486
|
+
self.input_type = input_type
|
|
487
|
+
for option in self._all_options:
|
|
488
|
+
self.validate_entered_text(option._default_text)
|
|
489
|
+
|
|
490
|
+
def validate_entered_text(self, entered_text: str) -> str:
|
|
491
|
+
if self.input_type == "number":
|
|
492
|
+
try:
|
|
493
|
+
float(entered_text)
|
|
494
|
+
except ValueError:
|
|
495
|
+
raise self._invalid_input_error(entered_text, "Must be a number")
|
|
496
|
+
elif self.input_type == "date":
|
|
497
|
+
try:
|
|
498
|
+
datetime.strptime(entered_text, "%Y-%m-%d")
|
|
499
|
+
except ValueError:
|
|
500
|
+
raise self._invalid_input_error(entered_text, "Must be a date in YYYY-MM-DD format")
|
|
501
|
+
elif self.input_type == "datetime-local":
|
|
502
|
+
try:
|
|
503
|
+
datetime.strptime(entered_text, "%Y-%m-%dT%H:%M")
|
|
504
|
+
except ValueError:
|
|
505
|
+
raise self._invalid_input_error(entered_text, "Must be a date in YYYY-MM-DDThh:mm format (e.g. 2020-01-01T07:00)")
|
|
506
|
+
elif self.input_type == "month":
|
|
507
|
+
try:
|
|
508
|
+
datetime.strptime(entered_text, "%Y-%m")
|
|
509
|
+
except ValueError:
|
|
510
|
+
raise self._invalid_input_error(entered_text, "Must be a date in YYYY-MM format")
|
|
511
|
+
elif self.input_type == "time":
|
|
512
|
+
try:
|
|
513
|
+
datetime.strptime(entered_text, "%H:%M")
|
|
514
|
+
except ValueError:
|
|
515
|
+
raise self._invalid_input_error(entered_text, "Must be a time in hh:mm format.")
|
|
516
|
+
elif self.input_type == "color":
|
|
517
|
+
if not re.match(c.COLOR_REGEX, entered_text):
|
|
518
|
+
raise self._invalid_input_error(entered_text, "Must be a valid color hex code (e.g. #000000).")
|
|
519
|
+
|
|
520
|
+
return entered_text
|
|
521
|
+
|
|
522
|
+
@staticmethod
|
|
523
|
+
def widget_type() -> str:
|
|
524
|
+
return "text"
|
|
525
|
+
|
|
526
|
+
@staticmethod
|
|
527
|
+
def ParameterOption(*args, **kwargs):
|
|
528
|
+
return po.TextParameterOption(*args, **kwargs)
|
|
529
|
+
|
|
530
|
+
@staticmethod
|
|
531
|
+
def DataSource(*args, **kwargs):
|
|
532
|
+
return d.TextDataSource(*args, **kwargs)
|
|
533
|
+
|
|
534
|
+
def with_selection(
|
|
535
|
+
self, selection: str | None, user: AbstractUser, parent_param: p._SelectionParameter | None
|
|
536
|
+
) -> p.TextParameter:
|
|
537
|
+
curr_option: po.TextParameterOption | None = next(self._get_options_iterator(self.all_options, user, parent_param), None)
|
|
538
|
+
entered_text = curr_option._default_text if selection is None and curr_option is not None else selection
|
|
539
|
+
return p.TextParameter(self, curr_option, entered_text)
|
|
540
|
+
|
|
541
|
+
def get_api_field_info(self) -> APIParamFieldInfo:
|
|
542
|
+
examples = [x._default_text for x in self.all_options]
|
|
543
|
+
return APIParamFieldInfo(
|
|
544
|
+
self.name, str, title=self.label, description=self.description, examples=examples
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
ParamConfigType = TypeVar("ParamConfigType", bound=ParameterConfig)
|
|
549
|
+
|
|
550
|
+
class DataSourceParameterConfig(Generic[ParamConfigType], ParameterConfigBase):
|
|
551
|
+
"""
|
|
552
|
+
Class to define configurations for parameter widgets whose options come from lookup tables
|
|
553
|
+
"""
|
|
554
|
+
def __init__(
|
|
555
|
+
self, parameter_type: type[ParamConfigType], name: str, label: str, data_source: d.DataSource | dict, *,
|
|
556
|
+
extra_args: dict = {}, description: str = "", user_attribute: str | None = None, parent_name: str | None = None
|
|
557
|
+
) -> None:
|
|
558
|
+
super().__init__(name, label, description=description, user_attribute=user_attribute, parent_name=parent_name)
|
|
559
|
+
self.parameter_type = parameter_type
|
|
560
|
+
if isinstance(data_source, dict):
|
|
561
|
+
if "source" in data_source:
|
|
562
|
+
data_source["source"] = d.SourceEnum(data_source["source"])
|
|
563
|
+
data_source = parameter_type.DataSource(**data_source)
|
|
564
|
+
self.data_source = data_source
|
|
565
|
+
self.extra_args = extra_args
|
|
566
|
+
|
|
567
|
+
def convert(self, df: pl.DataFrame) -> ParamConfigType:
|
|
568
|
+
return self.data_source._convert(self, df)
|
|
569
|
+
|
|
570
|
+
def get_dataframe(self, default_conn_name: str, conn_set: ConnectionSet, seeds: Seeds, datalake_db_path: str = "") -> pl.DataFrame:
|
|
571
|
+
datasource = self.data_source
|
|
572
|
+
query = datasource._get_query()
|
|
573
|
+
if datasource._source == d.SourceEnum.SEEDS:
|
|
574
|
+
df = seeds.run_query(query)
|
|
575
|
+
elif datasource._source == d.SourceEnum.VDL:
|
|
576
|
+
vdl_conn = u.create_duckdb_connection(datalake_db_path)
|
|
577
|
+
try:
|
|
578
|
+
# Query the VDL database
|
|
579
|
+
df = vdl_conn.sql(query).pl()
|
|
580
|
+
except Exception as e:
|
|
581
|
+
raise u.ConfigurationError(f'Error executing query for datasource parameter "{self.name}" from VDL') from e
|
|
582
|
+
finally:
|
|
583
|
+
vdl_conn.close()
|
|
584
|
+
else: # source == "connection"
|
|
585
|
+
conn_name = None
|
|
586
|
+
try:
|
|
587
|
+
conn_name = datasource._get_connection_name(default_conn_name)
|
|
588
|
+
df = conn_set.run_sql_query_from_conn_name(query, conn_name)
|
|
589
|
+
except RuntimeError as e:
|
|
590
|
+
ending = f' "{conn_name}"' if conn_name is not None else ""
|
|
591
|
+
raise u.ConfigurationError(f'Error executing query for datasource parameter "{self.name}" from connection{ending}') from e
|
|
592
|
+
return df
|