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.
Files changed (127) hide show
  1. dateutils/__init__.py +6 -0
  2. dateutils/_enums.py +25 -0
  3. squirrels/dateutils.py → dateutils/_implementation.py +409 -380
  4. dateutils/types.py +6 -0
  5. squirrels/__init__.py +21 -18
  6. squirrels/_api_routes/__init__.py +5 -0
  7. squirrels/_api_routes/auth.py +337 -0
  8. squirrels/_api_routes/base.py +196 -0
  9. squirrels/_api_routes/dashboards.py +156 -0
  10. squirrels/_api_routes/data_management.py +148 -0
  11. squirrels/_api_routes/datasets.py +220 -0
  12. squirrels/_api_routes/project.py +289 -0
  13. squirrels/_api_server.py +552 -134
  14. squirrels/_arguments/__init__.py +0 -0
  15. squirrels/_arguments/init_time_args.py +83 -0
  16. squirrels/_arguments/run_time_args.py +111 -0
  17. squirrels/_auth.py +777 -0
  18. squirrels/_command_line.py +239 -107
  19. squirrels/_compile_prompts.py +147 -0
  20. squirrels/_connection_set.py +94 -0
  21. squirrels/_constants.py +141 -64
  22. squirrels/_dashboards.py +179 -0
  23. squirrels/_data_sources.py +570 -0
  24. squirrels/_dataset_types.py +91 -0
  25. squirrels/_env_vars.py +209 -0
  26. squirrels/_exceptions.py +29 -0
  27. squirrels/_http_error_responses.py +52 -0
  28. squirrels/_initializer.py +319 -110
  29. squirrels/_logging.py +121 -0
  30. squirrels/_manifest.py +357 -187
  31. squirrels/_mcp_server.py +578 -0
  32. squirrels/_model_builder.py +69 -0
  33. squirrels/_model_configs.py +74 -0
  34. squirrels/_model_queries.py +52 -0
  35. squirrels/_models.py +1201 -0
  36. squirrels/_package_data/base_project/.env +7 -0
  37. squirrels/_package_data/base_project/.env.example +44 -0
  38. squirrels/_package_data/base_project/connections.yml +16 -0
  39. squirrels/_package_data/base_project/dashboards/dashboard_example.py +40 -0
  40. squirrels/_package_data/base_project/dashboards/dashboard_example.yml +22 -0
  41. squirrels/_package_data/base_project/docker/.dockerignore +16 -0
  42. squirrels/_package_data/base_project/docker/Dockerfile +16 -0
  43. squirrels/_package_data/base_project/docker/compose.yml +7 -0
  44. squirrels/_package_data/base_project/duckdb_init.sql +10 -0
  45. squirrels/_package_data/base_project/gitignore +13 -0
  46. squirrels/_package_data/base_project/macros/macros_example.sql +17 -0
  47. squirrels/_package_data/base_project/models/builds/build_example.py +26 -0
  48. squirrels/_package_data/base_project/models/builds/build_example.sql +16 -0
  49. squirrels/_package_data/base_project/models/builds/build_example.yml +57 -0
  50. squirrels/_package_data/base_project/models/dbviews/dbview_example.sql +17 -0
  51. squirrels/_package_data/base_project/models/dbviews/dbview_example.yml +32 -0
  52. squirrels/_package_data/base_project/models/federates/federate_example.py +51 -0
  53. squirrels/_package_data/base_project/models/federates/federate_example.sql +21 -0
  54. squirrels/_package_data/base_project/models/federates/federate_example.yml +65 -0
  55. squirrels/_package_data/base_project/models/sources.yml +38 -0
  56. squirrels/_package_data/base_project/parameters.yml +142 -0
  57. squirrels/_package_data/base_project/pyconfigs/connections.py +19 -0
  58. squirrels/_package_data/base_project/pyconfigs/context.py +96 -0
  59. squirrels/_package_data/base_project/pyconfigs/parameters.py +141 -0
  60. squirrels/_package_data/base_project/pyconfigs/user.py +56 -0
  61. squirrels/_package_data/base_project/resources/expenses.db +0 -0
  62. squirrels/_package_data/base_project/resources/public/.gitkeep +0 -0
  63. squirrels/_package_data/base_project/resources/weather.db +0 -0
  64. squirrels/_package_data/base_project/seeds/seed_categories.csv +6 -0
  65. squirrels/_package_data/base_project/seeds/seed_categories.yml +15 -0
  66. squirrels/_package_data/base_project/seeds/seed_subcategories.csv +15 -0
  67. squirrels/_package_data/base_project/seeds/seed_subcategories.yml +21 -0
  68. squirrels/_package_data/base_project/squirrels.yml.j2 +61 -0
  69. squirrels/_package_data/base_project/tmp/.gitignore +2 -0
  70. squirrels/_package_data/templates/login_successful.html +53 -0
  71. squirrels/_package_data/templates/squirrels_studio.html +22 -0
  72. squirrels/_package_loader.py +29 -0
  73. squirrels/_parameter_configs.py +592 -0
  74. squirrels/_parameter_options.py +348 -0
  75. squirrels/_parameter_sets.py +207 -0
  76. squirrels/_parameters.py +1703 -0
  77. squirrels/_project.py +796 -0
  78. squirrels/_py_module.py +122 -0
  79. squirrels/_request_context.py +33 -0
  80. squirrels/_schemas/__init__.py +0 -0
  81. squirrels/_schemas/auth_models.py +83 -0
  82. squirrels/_schemas/query_param_models.py +70 -0
  83. squirrels/_schemas/request_models.py +26 -0
  84. squirrels/_schemas/response_models.py +286 -0
  85. squirrels/_seeds.py +97 -0
  86. squirrels/_sources.py +112 -0
  87. squirrels/_utils.py +540 -149
  88. squirrels/_version.py +1 -3
  89. squirrels/arguments.py +7 -0
  90. squirrels/auth.py +4 -0
  91. squirrels/connections.py +3 -0
  92. squirrels/dashboards.py +3 -0
  93. squirrels/data_sources.py +14 -282
  94. squirrels/parameter_options.py +13 -189
  95. squirrels/parameters.py +14 -801
  96. squirrels/types.py +18 -0
  97. squirrels-0.6.0.post0.dist-info/METADATA +148 -0
  98. squirrels-0.6.0.post0.dist-info/RECORD +101 -0
  99. {squirrels-0.1.0.dist-info → squirrels-0.6.0.post0.dist-info}/WHEEL +1 -2
  100. {squirrels-0.1.0.dist-info → squirrels-0.6.0.post0.dist-info}/entry_points.txt +1 -0
  101. squirrels-0.6.0.post0.dist-info/licenses/LICENSE +201 -0
  102. squirrels/_credentials_manager.py +0 -87
  103. squirrels/_module_loader.py +0 -37
  104. squirrels/_parameter_set.py +0 -151
  105. squirrels/_renderer.py +0 -286
  106. squirrels/_timed_imports.py +0 -37
  107. squirrels/connection_set.py +0 -126
  108. squirrels/package_data/base_project/.gitignore +0 -4
  109. squirrels/package_data/base_project/connections.py +0 -21
  110. squirrels/package_data/base_project/database/sample_database.db +0 -0
  111. squirrels/package_data/base_project/database/seattle_weather.db +0 -0
  112. squirrels/package_data/base_project/datasets/sample_dataset/context.py +0 -8
  113. squirrels/package_data/base_project/datasets/sample_dataset/database_view1.py +0 -23
  114. squirrels/package_data/base_project/datasets/sample_dataset/database_view1.sql.j2 +0 -7
  115. squirrels/package_data/base_project/datasets/sample_dataset/final_view.py +0 -10
  116. squirrels/package_data/base_project/datasets/sample_dataset/final_view.sql.j2 +0 -2
  117. squirrels/package_data/base_project/datasets/sample_dataset/parameters.py +0 -30
  118. squirrels/package_data/base_project/datasets/sample_dataset/selections.cfg +0 -6
  119. squirrels/package_data/base_project/squirrels.yaml +0 -26
  120. squirrels/package_data/static/favicon.ico +0 -0
  121. squirrels/package_data/static/script.js +0 -234
  122. squirrels/package_data/static/style.css +0 -110
  123. squirrels/package_data/templates/index.html +0 -32
  124. squirrels-0.1.0.dist-info/LICENSE +0 -22
  125. squirrels-0.1.0.dist-info/METADATA +0 -67
  126. squirrels-0.1.0.dist-info/RECORD +0 -40
  127. 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