squirrels 0.1.1.post1__py3-none-any.whl → 0.2.0.dev0__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 -16
- squirrels/_api_server.py +234 -80
- squirrels/_authenticator.py +84 -0
- squirrels/_command_line.py +60 -72
- squirrels/_connection_set.py +96 -0
- squirrels/_constants.py +114 -33
- squirrels/_environcfg.py +77 -0
- squirrels/_initializer.py +126 -67
- squirrels/_manifest.py +195 -168
- squirrels/_models.py +495 -0
- squirrels/_package_loader.py +26 -0
- squirrels/_parameter_configs.py +401 -0
- squirrels/_parameter_sets.py +188 -0
- squirrels/_py_module.py +60 -0
- squirrels/_timer.py +36 -0
- squirrels/_utils.py +81 -49
- squirrels/_version.py +2 -2
- squirrels/arguments/init_time_args.py +32 -0
- squirrels/arguments/run_time_args.py +82 -0
- squirrels/data_sources.py +380 -155
- squirrels/dateutils.py +86 -57
- squirrels/package_data/base_project/Dockerfile +15 -0
- squirrels/package_data/base_project/connections.yml +7 -0
- squirrels/package_data/base_project/database/{sample_database.db → expenses.db} +0 -0
- squirrels/package_data/base_project/environcfg.yml +29 -0
- squirrels/package_data/base_project/ignores/.dockerignore +8 -0
- squirrels/package_data/base_project/ignores/.gitignore +7 -0
- squirrels/package_data/base_project/models/dbviews/database_view1.py +36 -0
- squirrels/package_data/base_project/models/dbviews/database_view1.sql +15 -0
- squirrels/package_data/base_project/models/federates/dataset_example.py +20 -0
- squirrels/package_data/base_project/models/federates/dataset_example.sql +3 -0
- squirrels/package_data/base_project/parameters.yml +109 -0
- squirrels/package_data/base_project/pyconfigs/auth.py +47 -0
- squirrels/package_data/base_project/pyconfigs/connections.py +28 -0
- squirrels/package_data/base_project/pyconfigs/context.py +45 -0
- squirrels/package_data/base_project/pyconfigs/parameters.py +55 -0
- squirrels/package_data/base_project/seeds/mocks/category.csv +3 -0
- squirrels/package_data/base_project/seeds/mocks/max_filter.csv +2 -0
- squirrels/package_data/base_project/seeds/mocks/subcategory.csv +6 -0
- squirrels/package_data/base_project/squirrels.yml.j2 +57 -0
- squirrels/package_data/base_project/tmp/.gitignore +2 -0
- squirrels/package_data/static/script.js +159 -63
- squirrels/package_data/static/style.css +79 -15
- squirrels/package_data/static/widgets.js +133 -0
- squirrels/package_data/templates/index.html +65 -23
- squirrels/package_data/templates/index2.html +22 -0
- squirrels/parameter_options.py +216 -119
- squirrels/parameters.py +407 -478
- squirrels/user_base.py +58 -0
- squirrels-0.2.0.dev0.dist-info/METADATA +126 -0
- squirrels-0.2.0.dev0.dist-info/RECORD +56 -0
- {squirrels-0.1.1.post1.dist-info → squirrels-0.2.0.dev0.dist-info}/WHEEL +1 -2
- squirrels-0.2.0.dev0.dist-info/entry_points.txt +3 -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 -20
- squirrels/package_data/base_project/datasets/sample_dataset/context.py +0 -22
- squirrels/package_data/base_project/datasets/sample_dataset/database_view1.py +0 -29
- squirrels/package_data/base_project/datasets/sample_dataset/database_view1.sql.j2 +0 -12
- squirrels/package_data/base_project/datasets/sample_dataset/final_view.py +0 -11
- squirrels/package_data/base_project/datasets/sample_dataset/final_view.sql.j2 +0 -3
- squirrels/package_data/base_project/datasets/sample_dataset/parameters.py +0 -47
- squirrels/package_data/base_project/datasets/sample_dataset/selections.cfg +0 -9
- squirrels/package_data/base_project/squirrels.yaml +0 -22
- squirrels-0.1.1.post1.dist-info/METADATA +0 -67
- squirrels-0.1.1.post1.dist-info/RECORD +0 -40
- squirrels-0.1.1.post1.dist-info/entry_points.txt +0 -2
- squirrels-0.1.1.post1.dist-info/top_level.txt +0 -1
- {squirrels-0.1.1.post1.dist-info → squirrels-0.2.0.dev0.dist-info}/LICENSE +0 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Type, Optional, Union, Sequence, Iterator
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from abc import ABCMeta, abstractmethod
|
|
5
|
+
from copy import copy
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
from . import parameter_options as po, parameters as p, data_sources as d, _utils as u
|
|
9
|
+
from ._authenticator import User
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class ParameterConfigBase(metaclass=ABCMeta):
|
|
14
|
+
"""
|
|
15
|
+
Abstract class for all parameter classes
|
|
16
|
+
"""
|
|
17
|
+
name: str
|
|
18
|
+
label: str
|
|
19
|
+
is_hidden: bool # = field(default=False, kw_only=True)
|
|
20
|
+
user_attribute: Optional[str] # = field(default=None, kw_only=True)
|
|
21
|
+
parent_name: Optional[str] # = field(default=None, kw_only=True)
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def __init__(
|
|
25
|
+
self, widget_type: str, name: str, label: str, *, is_hidden: bool = False, user_attribute: Optional[str] = None,
|
|
26
|
+
parent_name: Optional[str] = None, **kwargs
|
|
27
|
+
) -> None:
|
|
28
|
+
self.widget_type = widget_type
|
|
29
|
+
self.name = name
|
|
30
|
+
self.label = label
|
|
31
|
+
self.is_hidden = is_hidden
|
|
32
|
+
self.user_attribute = user_attribute
|
|
33
|
+
self.parent_name = parent_name
|
|
34
|
+
|
|
35
|
+
def _get_user_group(self, user: Optional[User]) -> Optional[str]:
|
|
36
|
+
if self.user_attribute is not None:
|
|
37
|
+
if user is None:
|
|
38
|
+
raise u.ConfigurationError(f"Public datasets with non-authenticated users cannot use parameter named " +
|
|
39
|
+
f"'{self.name}' because 'user_attribute' is defined on this parameter.")
|
|
40
|
+
return getattr(user, self.user_attribute)
|
|
41
|
+
|
|
42
|
+
def copy(self):
|
|
43
|
+
"""
|
|
44
|
+
Use for unit testing only
|
|
45
|
+
"""
|
|
46
|
+
return copy(self)
|
|
47
|
+
|
|
48
|
+
def to_json_dict0(self) -> dict:
|
|
49
|
+
return {
|
|
50
|
+
'widget_type': self.widget_type,
|
|
51
|
+
'name': self.name,
|
|
52
|
+
'label': self.label
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class ParameterConfig(ParameterConfigBase):
|
|
58
|
+
"""
|
|
59
|
+
Abstract class for all parameter classes (except DataSourceParameters)
|
|
60
|
+
"""
|
|
61
|
+
all_options: Sequence[po.ParameterOption] = field(repr=False)
|
|
62
|
+
|
|
63
|
+
@abstractmethod
|
|
64
|
+
def __init__(
|
|
65
|
+
self, widget_type: str, name: str, label: str, all_options: Sequence[Union[po.ParameterOption, dict]], *, is_hidden: bool = False,
|
|
66
|
+
user_attribute: Optional[str] = None, parent_name: Optional[str] = None, **kwargs
|
|
67
|
+
) -> None:
|
|
68
|
+
super().__init__(widget_type, name, label, is_hidden=is_hidden, user_attribute=user_attribute,
|
|
69
|
+
parent_name=parent_name)
|
|
70
|
+
self.all_options = tuple(self.__to_param_option(x) for x in all_options)
|
|
71
|
+
|
|
72
|
+
def __to_param_option(self, option: Union[po.ParameterOption, dict]) -> po.ParameterOption:
|
|
73
|
+
return self.__class__.ParameterOption(**option) if isinstance(option, dict) else option
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
@abstractmethod
|
|
77
|
+
def ParameterOption(*args, **kwargs) -> po.ParameterOption:
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
@abstractmethod
|
|
82
|
+
def DataSource(*args, **kwargs) -> d.DataSource:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
def _raise_invalid_input_error(self, selection: str, more_details: str = '', e: Exception = None) -> None:
|
|
86
|
+
raise u.InvalidInputError(f'Selected value "{selection}" is not valid for parameter "{self.name}". ' + more_details) from e
|
|
87
|
+
|
|
88
|
+
@abstractmethod
|
|
89
|
+
def with_selection(
|
|
90
|
+
self, selection: Optional[str], user: Optional[User], parent_param: Optional[p._SelectionParameter],
|
|
91
|
+
*, request_version: Optional[int] = None
|
|
92
|
+
) -> p.Parameter:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
def _get_options_iterator(self, user: Optional[User], parent_param: Optional[p._SelectionParameter]) -> Iterator[po.ParameterOption]:
|
|
96
|
+
user_group = self._get_user_group(user)
|
|
97
|
+
selected_parent_option_ids = frozenset(parent_param._get_selected_ids_as_list()) if parent_param else None
|
|
98
|
+
return (x for x in self.all_options if x._is_valid(user_group, selected_parent_option_ids))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class SelectionParameterConfig(ParameterConfig):
|
|
103
|
+
"""
|
|
104
|
+
Abstract class for select parameter classes (single-select, multi-select, etc)
|
|
105
|
+
"""
|
|
106
|
+
children: dict[str, ParameterConfigBase] = field(default_factory=dict, init=False, repr=False)
|
|
107
|
+
trigger_refresh: bool = field(default=False, init=False)
|
|
108
|
+
|
|
109
|
+
@abstractmethod
|
|
110
|
+
def __init__(
|
|
111
|
+
self, widget_type: str, name: str, label: str, all_options: Sequence[Union[po.SelectParameterOption, dict]], *, is_hidden: bool = False,
|
|
112
|
+
user_attribute: Optional[str] = None, parent_name: Optional[str] = None, **kwargs
|
|
113
|
+
) -> None:
|
|
114
|
+
super().__init__(widget_type, name, label, all_options, is_hidden=is_hidden, user_attribute=user_attribute,
|
|
115
|
+
parent_name=parent_name)
|
|
116
|
+
self.children: dict[str, ParameterConfigBase] = dict()
|
|
117
|
+
self.trigger_refresh = False
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def ParameterOption(*args, **kwargs):
|
|
121
|
+
return po.SelectParameterOption(*args, **kwargs)
|
|
122
|
+
|
|
123
|
+
def _add_child_mutate(self, child: ParameterConfigBase):
|
|
124
|
+
self.children[child.name] = child
|
|
125
|
+
self.trigger_refresh = True
|
|
126
|
+
|
|
127
|
+
def _get_options(self, user: Optional[User], parent_param: Optional[p._SelectionParameter]) -> Sequence[po.SelectParameterOption]:
|
|
128
|
+
return tuple(self._get_options_iterator(user, parent_param))
|
|
129
|
+
|
|
130
|
+
def _get_default_ids_iterator(self, options: Sequence[po.SelectParameterOption]) -> Iterator[str]:
|
|
131
|
+
return (x._identifier for x in options if x._is_default)
|
|
132
|
+
|
|
133
|
+
def copy(self) -> MultiSelectParameterConfig:
|
|
134
|
+
"""
|
|
135
|
+
Use for unit testing only
|
|
136
|
+
"""
|
|
137
|
+
other = super().copy()
|
|
138
|
+
other.children = self.children.copy()
|
|
139
|
+
return other
|
|
140
|
+
|
|
141
|
+
def to_json_dict0(self) -> dict:
|
|
142
|
+
output = super().to_json_dict0()
|
|
143
|
+
output['trigger_refresh'] = self.trigger_refresh
|
|
144
|
+
return output
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass
|
|
148
|
+
class SingleSelectParameterConfig(SelectionParameterConfig):
|
|
149
|
+
"""
|
|
150
|
+
Class to define configurations for single-select parameter widgets.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
def __init__(
|
|
154
|
+
self, name: str, label: str, all_options: Sequence[Union[po.SelectParameterOption, dict]], *, is_hidden: bool = False,
|
|
155
|
+
user_attribute: Optional[str] = None, parent_name: Optional[str] = None, **kwargs
|
|
156
|
+
) -> None:
|
|
157
|
+
super().__init__("single_select", name, label, all_options, is_hidden=is_hidden, user_attribute=user_attribute,
|
|
158
|
+
parent_name=parent_name)
|
|
159
|
+
|
|
160
|
+
@staticmethod
|
|
161
|
+
def DataSource(*args, **kwargs):
|
|
162
|
+
return d.SingleSelectDataSource(*args, **kwargs)
|
|
163
|
+
|
|
164
|
+
def with_selection(
|
|
165
|
+
self, selection: Optional[str], user: Optional[User], parent_param: Optional[p._SelectionParameter],
|
|
166
|
+
*, request_version: Optional[int] = None
|
|
167
|
+
) -> p.SingleSelectParameter:
|
|
168
|
+
options = self._get_options(user, parent_param)
|
|
169
|
+
if selection is None:
|
|
170
|
+
selected_id = next(self._get_default_ids_iterator(options), None)
|
|
171
|
+
if selected_id is None and len(options) > 0:
|
|
172
|
+
selected_id = options[0]._identifier
|
|
173
|
+
else:
|
|
174
|
+
selected_id = selection
|
|
175
|
+
return p.SingleSelectParameter(self, options, selected_id)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@dataclass
|
|
179
|
+
class MultiSelectParameterConfig(SelectionParameterConfig):
|
|
180
|
+
"""
|
|
181
|
+
Class to define configurations for multi-select parameter widgets.
|
|
182
|
+
"""
|
|
183
|
+
include_all: bool # = field(default=True, kw_only=True)
|
|
184
|
+
order_matters: bool # = field(default=False, kw_only=True)
|
|
185
|
+
|
|
186
|
+
def __init__(
|
|
187
|
+
self, name: str, label: str, all_options: Sequence[Union[po.SelectParameterOption, dict]], *, include_all: bool = True,
|
|
188
|
+
order_matters: bool = False, is_hidden: bool = False, user_attribute: Optional[str] = None, parent_name: Optional[str] = None,
|
|
189
|
+
**kwargs
|
|
190
|
+
) -> None:
|
|
191
|
+
super().__init__("multi_select", name, label, all_options, is_hidden=is_hidden, user_attribute=user_attribute,
|
|
192
|
+
parent_name=parent_name)
|
|
193
|
+
self.include_all = include_all
|
|
194
|
+
self.order_matters = order_matters
|
|
195
|
+
|
|
196
|
+
@staticmethod
|
|
197
|
+
def DataSource(*args, **kwargs):
|
|
198
|
+
return d.MultiSelectDataSource(*args, **kwargs)
|
|
199
|
+
|
|
200
|
+
def with_selection(
|
|
201
|
+
self, selection: Optional[str], user: Optional[User], parent_param: Optional[p._SelectionParameter],
|
|
202
|
+
*, request_version: Optional[int] = None
|
|
203
|
+
) -> p.MultiSelectParameter:
|
|
204
|
+
options = self._get_options(user, parent_param)
|
|
205
|
+
if selection is None:
|
|
206
|
+
selected_ids = tuple(self._get_default_ids_iterator(options))
|
|
207
|
+
else:
|
|
208
|
+
selected_ids = u.load_json_or_comma_delimited_str_as_list(selection)
|
|
209
|
+
return p.MultiSelectParameter(self, options, selected_ids)
|
|
210
|
+
|
|
211
|
+
def to_json_dict0(self) -> dict:
|
|
212
|
+
output = super().to_json_dict0()
|
|
213
|
+
output['include_all'] = self.include_all
|
|
214
|
+
output['order_matters'] = self.order_matters
|
|
215
|
+
return output
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@dataclass
|
|
219
|
+
class _DateTypeParameterConfig(ParameterConfig):
|
|
220
|
+
"""
|
|
221
|
+
Abstract class for date and date range parameter configs
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
@abstractmethod
|
|
225
|
+
def __init__(
|
|
226
|
+
self, widget_type: str, name: str, label: str, all_options: Sequence[Union[po.ParameterOption, dict]], *, is_hidden: bool = False,
|
|
227
|
+
user_attribute: Optional[str] = None, parent_name: Optional[str] = None, **kwargs
|
|
228
|
+
) -> None:
|
|
229
|
+
super().__init__(widget_type, name, label, all_options, is_hidden=is_hidden, user_attribute=user_attribute,
|
|
230
|
+
parent_name=parent_name)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@dataclass
|
|
234
|
+
class DateParameterConfig(_DateTypeParameterConfig):
|
|
235
|
+
"""
|
|
236
|
+
Class to define configurations for single-select parameter widgets.
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
def __init__(
|
|
240
|
+
self, name: str, label: str, all_options: Sequence[Union[po.DateParameterOption, dict]], *, is_hidden: bool = False,
|
|
241
|
+
user_attribute: Optional[str] = None, parent_name: Optional[str] = None, **kwargs
|
|
242
|
+
) -> None:
|
|
243
|
+
super().__init__("date", name, label, all_options, is_hidden=is_hidden, user_attribute=user_attribute,
|
|
244
|
+
parent_name=parent_name)
|
|
245
|
+
|
|
246
|
+
@staticmethod
|
|
247
|
+
def ParameterOption(*args, **kwargs):
|
|
248
|
+
return po.DateParameterOption(*args, **kwargs)
|
|
249
|
+
|
|
250
|
+
@staticmethod
|
|
251
|
+
def DataSource(*args, **kwargs):
|
|
252
|
+
return d.DateDataSource(*args, **kwargs)
|
|
253
|
+
|
|
254
|
+
def with_selection(
|
|
255
|
+
self, selection: Optional[str], user: Optional[User], parent_param: Optional[p._SelectionParameter],
|
|
256
|
+
*, request_version: Optional[int] = None
|
|
257
|
+
) -> p.DateParameter:
|
|
258
|
+
curr_option: po.DateParameterOption = next(self._get_options_iterator(user, parent_param))
|
|
259
|
+
selected_date = curr_option._default_date if selection is None else selection
|
|
260
|
+
return p.DateParameter(self, curr_option, selected_date)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@dataclass
|
|
264
|
+
class DateRangeParameterConfig(_DateTypeParameterConfig):
|
|
265
|
+
"""
|
|
266
|
+
Class to define configurations for single-select parameter widgets.
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
def __init__(
|
|
270
|
+
self, name: str, label: str, all_options: Sequence[Union[po.DateRangeParameterOption, dict]], *, is_hidden: bool = False,
|
|
271
|
+
user_attribute: Optional[str] = None, parent_name: Optional[str] = None, **kwargs
|
|
272
|
+
) -> None:
|
|
273
|
+
super().__init__("date_range", name, label, all_options, is_hidden=is_hidden, user_attribute=user_attribute,
|
|
274
|
+
parent_name=parent_name)
|
|
275
|
+
|
|
276
|
+
@staticmethod
|
|
277
|
+
def ParameterOption(*args, **kwargs):
|
|
278
|
+
return po.DateRangeParameterOption(*args, **kwargs)
|
|
279
|
+
|
|
280
|
+
@staticmethod
|
|
281
|
+
def DataSource(*args, **kwargs):
|
|
282
|
+
return d.DateRangeDataSource(*args, **kwargs)
|
|
283
|
+
|
|
284
|
+
def with_selection(
|
|
285
|
+
self, selection: Optional[str], user: Optional[User], parent_param: Optional[p._SelectionParameter],
|
|
286
|
+
*, request_version: Optional[int] = None
|
|
287
|
+
) -> p.DateParameter:
|
|
288
|
+
curr_option: po.DateRangeParameterOption = next(self._get_options_iterator(user, parent_param))
|
|
289
|
+
if selection is None:
|
|
290
|
+
selected_start_date = curr_option._default_start_date
|
|
291
|
+
selected_end_date = curr_option._default_end_date
|
|
292
|
+
else:
|
|
293
|
+
try:
|
|
294
|
+
selected_start_date, selected_end_date = u.load_json_or_comma_delimited_str_as_list(selection)
|
|
295
|
+
except ValueError as e:
|
|
296
|
+
self._raise_invalid_input_error(selection, "Date range parameter selection must be two dates joined by comma.", e)
|
|
297
|
+
return p.DateRangeParameter(self, curr_option, selected_start_date, selected_end_date)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@dataclass
|
|
301
|
+
class _NumericParameterConfig(ParameterConfig):
|
|
302
|
+
"""
|
|
303
|
+
Abstract class for number and number range parameter configs
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
@abstractmethod
|
|
307
|
+
def __init__(
|
|
308
|
+
self, widget_type: str, name: str, label: str, all_options: Sequence[Union[po.ParameterOption, dict]], *, is_hidden: bool = False,
|
|
309
|
+
user_attribute: Optional[str] = None, parent_name: Optional[str] = None, **kwargs
|
|
310
|
+
) -> None:
|
|
311
|
+
super().__init__(widget_type, name, label, all_options, is_hidden=is_hidden, user_attribute=user_attribute,
|
|
312
|
+
parent_name=parent_name)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@dataclass
|
|
316
|
+
class NumberParameterConfig(_NumericParameterConfig):
|
|
317
|
+
"""
|
|
318
|
+
Class to define configurations for single-select parameter widgets.
|
|
319
|
+
"""
|
|
320
|
+
|
|
321
|
+
def __init__(
|
|
322
|
+
self, name: str, label: str, all_options: Sequence[Union[po.NumberParameterOption, dict]], *, is_hidden: bool = False,
|
|
323
|
+
user_attribute: Optional[str] = None, parent_name: Optional[str] = None, **kwargs
|
|
324
|
+
) -> None:
|
|
325
|
+
super().__init__("number", name, label, all_options, is_hidden=is_hidden, user_attribute=user_attribute,
|
|
326
|
+
parent_name=parent_name)
|
|
327
|
+
|
|
328
|
+
@staticmethod
|
|
329
|
+
def ParameterOption(*args, **kwargs):
|
|
330
|
+
return po.NumberParameterOption(*args, **kwargs)
|
|
331
|
+
|
|
332
|
+
@staticmethod
|
|
333
|
+
def DataSource(*args, **kwargs):
|
|
334
|
+
return d.NumberDataSource(*args, **kwargs)
|
|
335
|
+
|
|
336
|
+
def with_selection(
|
|
337
|
+
self, selection: Optional[str], user: Optional[User], parent_param: Optional[p._SelectionParameter],
|
|
338
|
+
*, request_version: Optional[int] = None
|
|
339
|
+
) -> p.NumberParameter:
|
|
340
|
+
curr_option: po.NumberParameterOption = next(self._get_options_iterator(user, parent_param))
|
|
341
|
+
selected_value = curr_option._default_value if selection is None else selection
|
|
342
|
+
return p.NumberParameter(self, curr_option, selected_value)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@dataclass
|
|
346
|
+
class NumberRangeParameterConfig(_NumericParameterConfig):
|
|
347
|
+
"""
|
|
348
|
+
Class to define configurations for single-select parameter widgets.
|
|
349
|
+
"""
|
|
350
|
+
|
|
351
|
+
def __init__(
|
|
352
|
+
self, name: str, label: str, all_options: Sequence[Union[po.NumberRangeParameterOption, dict]], *, is_hidden: bool = False,
|
|
353
|
+
user_attribute: Optional[str] = None, parent_name: Optional[str] = None, **kwargs
|
|
354
|
+
) -> None:
|
|
355
|
+
super().__init__("number_range", name, label, all_options, is_hidden=is_hidden, user_attribute=user_attribute,
|
|
356
|
+
parent_name=parent_name)
|
|
357
|
+
|
|
358
|
+
@staticmethod
|
|
359
|
+
def ParameterOption(*args, **kwargs):
|
|
360
|
+
return po.NumberRangeParameterOption(*args, **kwargs)
|
|
361
|
+
|
|
362
|
+
@staticmethod
|
|
363
|
+
def DataSource(*args, **kwargs):
|
|
364
|
+
return d.NumberRangeDataSource(*args, **kwargs)
|
|
365
|
+
|
|
366
|
+
def with_selection(
|
|
367
|
+
self, selection: Optional[str], user: Optional[User], parent_param: Optional[p._SelectionParameter],
|
|
368
|
+
*, request_version: Optional[int] = None
|
|
369
|
+
) -> p.NumberRangeParameter:
|
|
370
|
+
curr_option: po.NumberRangeParameterOption = next(self._get_options_iterator(user, parent_param))
|
|
371
|
+
if selection is None:
|
|
372
|
+
selected_lower_value = curr_option._default_lower_value
|
|
373
|
+
selected_upper_value = curr_option._default_upper_value
|
|
374
|
+
else:
|
|
375
|
+
try:
|
|
376
|
+
selected_lower_value, selected_upper_value = u.load_json_or_comma_delimited_str_as_list(selection)
|
|
377
|
+
except ValueError as e:
|
|
378
|
+
self._raise_invalid_input_error(selection, "Number range parameter selection must be two numbers joined by comma.", e)
|
|
379
|
+
return p.NumberRangeParameter(self, curr_option, selected_lower_value, selected_upper_value)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@dataclass
|
|
383
|
+
class DataSourceParameterConfig(ParameterConfigBase):
|
|
384
|
+
"""
|
|
385
|
+
Class to define configurations for parameter widgets whose options come from lookup tables
|
|
386
|
+
"""
|
|
387
|
+
parameter_type: Type[ParameterConfig]
|
|
388
|
+
data_source: d.DataSource
|
|
389
|
+
|
|
390
|
+
def __init__(
|
|
391
|
+
self, parameter_type: Type[ParameterConfig], name: str, label: str, data_source: Union[d.DataSource, dict], *,
|
|
392
|
+
is_hidden: bool = False, user_attribute: Optional[str] = None, parent_name: Optional[str] = None, **kwargs
|
|
393
|
+
) -> None:
|
|
394
|
+
super().__init__("data_source", name, label, is_hidden=is_hidden, user_attribute=user_attribute, parent_name=parent_name)
|
|
395
|
+
self.parameter_type = parameter_type
|
|
396
|
+
if isinstance(data_source, dict):
|
|
397
|
+
data_source = parameter_type.DataSource(**data_source)
|
|
398
|
+
self.data_source = data_source
|
|
399
|
+
|
|
400
|
+
def convert(self, df: pd.DataFrame) -> ParameterConfig:
|
|
401
|
+
return self.data_source._convert(self, df)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Optional, Sequence
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from collections import OrderedDict
|
|
5
|
+
import concurrent.futures, pandas as pd
|
|
6
|
+
|
|
7
|
+
from . import _utils as u, _constants as c, parameters as p, _parameter_configs as pc, _py_module as pm
|
|
8
|
+
from .arguments.init_time_args import ParametersArgs
|
|
9
|
+
from ._manifest import ManifestIO, ParametersConfig
|
|
10
|
+
from ._connection_set import ConnectionSetIO
|
|
11
|
+
from ._authenticator import User
|
|
12
|
+
from ._timer import timer, time
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class ParameterSet:
|
|
17
|
+
"""
|
|
18
|
+
A wrapper class for a sequence of parameters with the selections applied as well
|
|
19
|
+
"""
|
|
20
|
+
_parameters_dict: OrderedDict[str, p.Parameter]
|
|
21
|
+
|
|
22
|
+
def get_parameters_as_dict(self) -> dict[str, p.Parameter]:
|
|
23
|
+
return self._parameters_dict.copy()
|
|
24
|
+
|
|
25
|
+
def to_json_dict0(self, *, debug: bool = False) -> dict:
|
|
26
|
+
parameters = []
|
|
27
|
+
for x in self._parameters_dict.values():
|
|
28
|
+
if not x._config.is_hidden or debug:
|
|
29
|
+
parameters.append(x.to_json_dict0())
|
|
30
|
+
return {"parameters": parameters}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class _ParameterConfigsSet:
|
|
35
|
+
"""
|
|
36
|
+
Pool of parameter configs, can create multiple for unit testing purposes
|
|
37
|
+
"""
|
|
38
|
+
_data: dict[str, pc.ParameterConfig] = field(default_factory=OrderedDict)
|
|
39
|
+
_data_source_params: dict[str, pc.DataSourceParameterConfig] = field(default_factory=dict)
|
|
40
|
+
|
|
41
|
+
def get(self, name: Optional[str]) -> Optional[pc.ParameterConfig]:
|
|
42
|
+
try:
|
|
43
|
+
return self._data[name] if name is not None else None
|
|
44
|
+
except KeyError as e:
|
|
45
|
+
raise u.ConfigurationError(f'Unable to find parameter named "{name}"') from e
|
|
46
|
+
|
|
47
|
+
def add(self, param_config: pc.ParameterConfigBase) -> None:
|
|
48
|
+
self._data[param_config.name] = param_config
|
|
49
|
+
if isinstance(param_config, pc.DataSourceParameterConfig):
|
|
50
|
+
self._data_source_params[param_config.name] = param_config
|
|
51
|
+
|
|
52
|
+
def _get_all_ds_param_configs(self) -> Sequence[pc.DataSourceParameterConfig]:
|
|
53
|
+
return list(self._data_source_params.values())
|
|
54
|
+
|
|
55
|
+
def __convert_datasource_params(self, df_dict: dict[str, pd.DataFrame]) -> None:
|
|
56
|
+
done = set()
|
|
57
|
+
for curr_name in self._data_source_params:
|
|
58
|
+
stack = [curr_name] # Note: parents must be converted first before children
|
|
59
|
+
while stack:
|
|
60
|
+
name = stack[-1]
|
|
61
|
+
if name not in done:
|
|
62
|
+
param = self._data_source_params.get(name, self.get(name))
|
|
63
|
+
parent_name = param.parent_name
|
|
64
|
+
if parent_name is not None and parent_name not in done:
|
|
65
|
+
stack.append(parent_name)
|
|
66
|
+
continue
|
|
67
|
+
if isinstance(param, pc.DataSourceParameterConfig):
|
|
68
|
+
if name not in df_dict:
|
|
69
|
+
raise u.ConfigurationError(f'No reference data found for parameter "{name}"')
|
|
70
|
+
self._data[name] = param.convert(df_dict[name])
|
|
71
|
+
done.add(name)
|
|
72
|
+
stack.pop()
|
|
73
|
+
|
|
74
|
+
def __validate_param_relationships(self) -> None:
|
|
75
|
+
for param_config in self._data.values():
|
|
76
|
+
assert isinstance(param_config, pc.ParameterConfig)
|
|
77
|
+
parent_name = param_config.parent_name
|
|
78
|
+
parent = self.get(parent_name)
|
|
79
|
+
if parent:
|
|
80
|
+
if not isinstance(param_config, pc.SelectionParameterConfig):
|
|
81
|
+
if not isinstance(parent, pc.SingleSelectParameterConfig):
|
|
82
|
+
raise u.ConfigurationError(f'Only single-select parameters can be parents of non-select parameters. ' +
|
|
83
|
+
f'Parameter "{parent_name}" is the parent of non-select parameter ' +
|
|
84
|
+
f'"{param_config.name}" but "{parent_name}" is not a single-select parameter.')
|
|
85
|
+
seen = set()
|
|
86
|
+
for option in param_config.all_options:
|
|
87
|
+
lookup_keys = option._parent_option_ids
|
|
88
|
+
if len(option._user_groups) > 0:
|
|
89
|
+
lookup_keys = set((x, y) for x in option._parent_option_ids for y in option._user_groups)
|
|
90
|
+
if not seen.isdisjoint(lookup_keys):
|
|
91
|
+
raise u.ConfigurationError(f'Each distinct value of "parent option id" can only appear once (per user group)' +
|
|
92
|
+
f'among the options of non-select parameter "{param_config.name}".')
|
|
93
|
+
seen.update(lookup_keys)
|
|
94
|
+
|
|
95
|
+
if not isinstance(parent, pc.SelectionParameterConfig):
|
|
96
|
+
raise u.ConfigurationError(f'Only selection parameters can be parents. Parameter "{parent_name}" is the parent of ' +
|
|
97
|
+
f'"{param_config.name}" but "{parent_name}" is not a selection parameter.')
|
|
98
|
+
|
|
99
|
+
parent._add_child_mutate(param_config)
|
|
100
|
+
|
|
101
|
+
def _post_process_params(self, df_dict: dict[str, pd.DataFrame]) -> None:
|
|
102
|
+
self.__convert_datasource_params(df_dict)
|
|
103
|
+
self.__validate_param_relationships()
|
|
104
|
+
|
|
105
|
+
def apply_selections(
|
|
106
|
+
self, dataset_params: Optional[Sequence[str]], selections: dict[str, str], user: Optional[User],
|
|
107
|
+
*, updates_only: bool = False, request_version: Optional[int] = None
|
|
108
|
+
) -> ParameterSet:
|
|
109
|
+
if dataset_params is None:
|
|
110
|
+
dataset_params = self._data.keys()
|
|
111
|
+
|
|
112
|
+
parameters_by_name: dict[str, p.Parameter] = {}
|
|
113
|
+
params_to_process = selections.keys() if selections and updates_only else dataset_params
|
|
114
|
+
params_to_process_set = set(params_to_process)
|
|
115
|
+
for some_name in params_to_process:
|
|
116
|
+
stack = [some_name] # Note: process parent selections first (if applicable) before children
|
|
117
|
+
while stack:
|
|
118
|
+
curr_name = stack[-1]
|
|
119
|
+
children = []
|
|
120
|
+
if curr_name not in parameters_by_name:
|
|
121
|
+
param_conf = self.get(curr_name)
|
|
122
|
+
parent_name = param_conf.parent_name
|
|
123
|
+
if parent_name is None:
|
|
124
|
+
parent = None
|
|
125
|
+
elif parent_name in params_to_process_set and parent_name not in parameters_by_name:
|
|
126
|
+
stack.append(parent_name)
|
|
127
|
+
continue
|
|
128
|
+
else:
|
|
129
|
+
parent = parameters_by_name.get(parent_name)
|
|
130
|
+
param = param_conf.with_selection(selections.get(curr_name), user, parent)
|
|
131
|
+
parameters_by_name[curr_name] = param
|
|
132
|
+
if isinstance(param_conf, pc.SelectionParameterConfig):
|
|
133
|
+
children = list(param_conf.children.keys())
|
|
134
|
+
stack.pop()
|
|
135
|
+
stack.extend(children)
|
|
136
|
+
|
|
137
|
+
ordered_parameters = OrderedDict((key, parameters_by_name[key]) for key in dataset_params if key in parameters_by_name)
|
|
138
|
+
return ParameterSet(ordered_parameters)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class ParameterConfigsSetIO:
|
|
142
|
+
"""
|
|
143
|
+
Static class for the singleton object of __ParameterConfigsPoolData
|
|
144
|
+
"""
|
|
145
|
+
args: ParametersArgs
|
|
146
|
+
obj: _ParameterConfigsSet
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def _GetDfDict(cls) -> dict[str, pd.DataFrame]:
|
|
150
|
+
def get_dataframe_from_query(ds_param_config: pc.DataSourceParameterConfig) -> pd.DataFrame:
|
|
151
|
+
key, datasource = ds_param_config.name, ds_param_config.data_source
|
|
152
|
+
try:
|
|
153
|
+
query, conn_name = datasource._get_query(), datasource._get_connection_name()
|
|
154
|
+
df = ConnectionSetIO.obj.run_sql_query_from_conn_name(query, conn_name)
|
|
155
|
+
except RuntimeError as e:
|
|
156
|
+
raise u.ConfigurationError(f'Error executing query for datasource parameter "{key}"') from e
|
|
157
|
+
return key, df
|
|
158
|
+
|
|
159
|
+
ds_param_configs = cls.obj._get_all_ds_param_configs()
|
|
160
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
161
|
+
df_dict = dict(executor.map(get_dataframe_from_query, ds_param_configs))
|
|
162
|
+
|
|
163
|
+
return df_dict
|
|
164
|
+
|
|
165
|
+
@classmethod
|
|
166
|
+
def _AddFromDict(cls, param_config: ParametersConfig) -> None:
|
|
167
|
+
param_config.arguments["name"] = param_config.name
|
|
168
|
+
ptype = getattr(p, param_config.type)
|
|
169
|
+
factory = getattr(ptype, param_config.factory)
|
|
170
|
+
factory(**param_config.arguments)
|
|
171
|
+
|
|
172
|
+
@classmethod
|
|
173
|
+
def LoadFromFile(cls) -> None:
|
|
174
|
+
start = time.time()
|
|
175
|
+
cls.obj = _ParameterConfigsSet()
|
|
176
|
+
|
|
177
|
+
parameters_from_manifest = ManifestIO.obj.parameters
|
|
178
|
+
for param_as_dict in parameters_from_manifest:
|
|
179
|
+
cls._AddFromDict(param_as_dict)
|
|
180
|
+
|
|
181
|
+
conn_args = ConnectionSetIO.args
|
|
182
|
+
cls.args = ParametersArgs(conn_args.proj_vars, conn_args.env_vars)
|
|
183
|
+
pm.run_pyconfig_main(c.PARAMETERS_FILE, {"sqrl": cls.args})
|
|
184
|
+
|
|
185
|
+
df_dict = cls._GetDfDict()
|
|
186
|
+
cls.obj._post_process_params(df_dict)
|
|
187
|
+
|
|
188
|
+
timer.add_activity_time("loading parameters", start)
|
squirrels/_py_module.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from typing import Type, Optional, Any
|
|
2
|
+
from types import ModuleType
|
|
3
|
+
from importlib.machinery import SourceFileLoader
|
|
4
|
+
|
|
5
|
+
from . import _constants as c, _utils as u
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PyModule:
|
|
9
|
+
def __init__(self, filepath: u.FilePath, *, default_class: Optional[Type] = None, is_required: bool = False) -> None:
|
|
10
|
+
"""
|
|
11
|
+
Constructor for PyModule, an abstract module for a file that may or may not exist
|
|
12
|
+
|
|
13
|
+
Parameters:
|
|
14
|
+
filepath (str | pathlib.Path): The file path to the python module
|
|
15
|
+
is_required: If true, throw an error if the file path doesn't exist
|
|
16
|
+
"""
|
|
17
|
+
self.filepath = str(filepath)
|
|
18
|
+
try:
|
|
19
|
+
self.module: Optional[ModuleType] = SourceFileLoader(self.filepath, self.filepath).load_module()
|
|
20
|
+
except FileNotFoundError as e:
|
|
21
|
+
if is_required:
|
|
22
|
+
raise u.ConfigurationError(f"Required file not found: '{self.filepath}'") from e
|
|
23
|
+
self.module: Optional[ModuleType] = default_class
|
|
24
|
+
|
|
25
|
+
def get_func_or_class(self, attr_name: str, *, default_attr: Any = None, is_required: bool = True) -> Any:
|
|
26
|
+
"""
|
|
27
|
+
Get an attribute of the module. Usually a python function or class.
|
|
28
|
+
|
|
29
|
+
Parameters:
|
|
30
|
+
attr_name: The attribute name
|
|
31
|
+
default_attr: The default function or class to use if the attribute cannot be found
|
|
32
|
+
is_required: If true, throw an error if the attribute cannot be found, unless default_attr is not None
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
The attribute of the module
|
|
36
|
+
"""
|
|
37
|
+
func_or_class = default_attr
|
|
38
|
+
if self.module is not None and hasattr(self.module, attr_name):
|
|
39
|
+
func_or_class = getattr(self.module, attr_name)
|
|
40
|
+
if func_or_class is None and is_required:
|
|
41
|
+
raise u.ConfigurationError(f"Module '{self.filepath}' missing required attribute '{attr_name}'")
|
|
42
|
+
return func_or_class
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def run_pyconfig_main(filename: str, kwargs: dict[str, Any] = {}) -> None:
|
|
46
|
+
"""
|
|
47
|
+
Given a python file in the 'pyconfigs' folder, run its main function
|
|
48
|
+
|
|
49
|
+
Parameters:
|
|
50
|
+
filename: The name of the file to run main function
|
|
51
|
+
kwargs: Dictionary of the main function arguments
|
|
52
|
+
"""
|
|
53
|
+
filepath = u.join_paths(c.PYCONFIG_FOLDER, filename)
|
|
54
|
+
module = PyModule(filepath)
|
|
55
|
+
main_function = module.get_func_or_class(c.MAIN_FUNC, is_required=False)
|
|
56
|
+
if main_function:
|
|
57
|
+
try:
|
|
58
|
+
main_function(**kwargs)
|
|
59
|
+
except Exception as e:
|
|
60
|
+
raise u.FileExecutionError(f'Failed to run python file "{filepath}"', e)
|
squirrels/_timer.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Timer:
|
|
6
|
+
def __init__(self):
|
|
7
|
+
# self.times: dict[str, list[float]] = dict()
|
|
8
|
+
self.verbose = False
|
|
9
|
+
|
|
10
|
+
def _get_dt_from_timestamp(self, timestamp) -> str:
|
|
11
|
+
return datetime.fromtimestamp(timestamp).strftime('%H:%M:%S.%f')
|
|
12
|
+
|
|
13
|
+
def add_activity_time(self, activity: str, start_timestamp: float) -> None:
|
|
14
|
+
if self.verbose:
|
|
15
|
+
end_timestamp = time.time()
|
|
16
|
+
time_taken = round((end_timestamp-start_timestamp) * 10**3, 3)
|
|
17
|
+
# times_list = self.times.setdefault(activity, list())
|
|
18
|
+
# times_list.append(time_taken)
|
|
19
|
+
print(f'Time taken for "{activity}": {time_taken}ms')
|
|
20
|
+
|
|
21
|
+
start_datetime = self._get_dt_from_timestamp(start_timestamp)
|
|
22
|
+
end_datetime = self._get_dt_from_timestamp(end_timestamp)
|
|
23
|
+
print(f'--> start time: "{start_datetime}", end time: "{end_datetime}"')
|
|
24
|
+
print()
|
|
25
|
+
|
|
26
|
+
# def report_times(self):
|
|
27
|
+
# if self.verbose:
|
|
28
|
+
# for activity, times_list in self.times.items():
|
|
29
|
+
# total_time = sum(times_list)
|
|
30
|
+
# avg_time = total_time / len(times_list)
|
|
31
|
+
# print()
|
|
32
|
+
# print(f'Time statistics for "{activity}":')
|
|
33
|
+
# print(f' Total time: {total_time}ms')
|
|
34
|
+
# print(f' Average time: {avg_time}ms')
|
|
35
|
+
|
|
36
|
+
timer = Timer()
|