squirrels 0.5.0b2__py3-none-any.whl → 0.5.0b4__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (96) hide show
  1. dateutils/__init__.py +6 -460
  2. dateutils/_enums.py +25 -0
  3. dateutils/_implementation.py +409 -0
  4. dateutils/types.py +6 -0
  5. squirrels/__init__.py +9 -13
  6. squirrels/_api_routes/__init__.py +5 -0
  7. squirrels/_api_routes/auth.py +262 -0
  8. squirrels/_api_routes/base.py +154 -0
  9. squirrels/_api_routes/dashboards.py +142 -0
  10. squirrels/_api_routes/data_management.py +103 -0
  11. squirrels/_api_routes/datasets.py +242 -0
  12. squirrels/_api_routes/oauth2.py +300 -0
  13. squirrels/_api_routes/project.py +214 -0
  14. squirrels/_api_server.py +145 -748
  15. squirrels/_arguments/__init__.py +0 -0
  16. squirrels/{arguments → _arguments}/init_time_args.py +7 -2
  17. squirrels/{arguments → _arguments}/run_time_args.py +4 -26
  18. squirrels/_auth.py +646 -93
  19. squirrels/_connection_set.py +5 -5
  20. squirrels/_constants.py +7 -1
  21. squirrels/{_dashboards_io.py → _dashboards.py} +87 -6
  22. squirrels/_data_sources.py +564 -0
  23. squirrels/_exceptions.py +9 -37
  24. squirrels/_initializer.py +31 -26
  25. squirrels/_manifest.py +5 -5
  26. squirrels/_model_builder.py +1 -1
  27. squirrels/_model_configs.py +2 -2
  28. squirrels/_model_queries.py +1 -1
  29. squirrels/_models.py +40 -27
  30. squirrels/{package_data → _package_data}/base_project/.env +1 -0
  31. squirrels/{package_data → _package_data}/base_project/.env.example +1 -0
  32. squirrels/{package_data → _package_data}/base_project/dashboards/dashboard_example.py +4 -4
  33. squirrels/{package_data → _package_data}/base_project/dashboards/dashboard_example.yml +2 -2
  34. squirrels/_package_data/base_project/macros/macros_example.sql +17 -0
  35. squirrels/{package_data → _package_data}/base_project/models/builds/build_example.py +2 -2
  36. squirrels/{package_data → _package_data}/base_project/models/builds/build_example.sql +1 -1
  37. squirrels/{package_data → _package_data}/base_project/models/dbviews/dbview_example.sql +1 -1
  38. squirrels/_package_data/base_project/models/federates/federate_example.py +41 -0
  39. squirrels/_package_data/base_project/models/federates/federate_example.sql +25 -0
  40. squirrels/{package_data → _package_data}/base_project/models/federates/federate_example.yml +6 -6
  41. squirrels/{package_data → _package_data}/base_project/parameters.yml +9 -8
  42. squirrels/_package_data/base_project/pyconfigs/connections.py +14 -0
  43. squirrels/{package_data → _package_data}/base_project/pyconfigs/context.py +14 -16
  44. squirrels/_package_data/base_project/pyconfigs/parameters.py +106 -0
  45. squirrels/_package_data/base_project/pyconfigs/user.py +51 -0
  46. squirrels/_package_data/templates/dataset_results.html +112 -0
  47. squirrels/_package_data/templates/oauth_login.html +271 -0
  48. squirrels/_parameter_configs.py +35 -35
  49. squirrels/_parameter_options.py +348 -0
  50. squirrels/_parameter_sets.py +47 -37
  51. squirrels/_parameters.py +1664 -0
  52. squirrels/_project.py +76 -32
  53. squirrels/_py_module.py +3 -2
  54. squirrels/_schemas/__init__.py +0 -0
  55. squirrels/_schemas/auth_models.py +144 -0
  56. squirrels/_schemas/query_param_models.py +67 -0
  57. squirrels/{_api_response_models.py → _schemas/response_models.py} +12 -8
  58. squirrels/_utils.py +38 -4
  59. squirrels/arguments.py +2 -0
  60. squirrels/auth.py +1 -0
  61. squirrels/connections.py +1 -0
  62. squirrels/dashboards.py +1 -82
  63. squirrels/data_sources.py +8 -563
  64. squirrels/parameter_options.py +8 -348
  65. squirrels/parameters.py +9 -1266
  66. squirrels/types.py +11 -0
  67. {squirrels-0.5.0b2.dist-info → squirrels-0.5.0b4.dist-info}/METADATA +4 -1
  68. squirrels-0.5.0b4.dist-info/RECORD +94 -0
  69. squirrels/package_data/base_project/macros/macros_example.sql +0 -15
  70. squirrels/package_data/base_project/models/federates/federate_example.py +0 -44
  71. squirrels/package_data/base_project/models/federates/federate_example.sql +0 -17
  72. squirrels/package_data/base_project/pyconfigs/connections.py +0 -14
  73. squirrels/package_data/base_project/pyconfigs/parameters.py +0 -93
  74. squirrels/package_data/base_project/pyconfigs/user.py +0 -23
  75. squirrels-0.5.0b2.dist-info/RECORD +0 -70
  76. /squirrels/{dataset_result.py → _dataset_types.py} +0 -0
  77. /squirrels/{package_data → _package_data}/base_project/assets/expenses.db +0 -0
  78. /squirrels/{package_data → _package_data}/base_project/assets/weather.db +0 -0
  79. /squirrels/{package_data → _package_data}/base_project/connections.yml +0 -0
  80. /squirrels/{package_data → _package_data}/base_project/docker/.dockerignore +0 -0
  81. /squirrels/{package_data → _package_data}/base_project/docker/Dockerfile +0 -0
  82. /squirrels/{package_data → _package_data}/base_project/docker/compose.yml +0 -0
  83. /squirrels/{package_data → _package_data}/base_project/duckdb_init.sql +0 -0
  84. /squirrels/{package_data/base_project/.gitignore → _package_data/base_project/gitignore} +0 -0
  85. /squirrels/{package_data → _package_data}/base_project/models/builds/build_example.yml +0 -0
  86. /squirrels/{package_data → _package_data}/base_project/models/dbviews/dbview_example.yml +0 -0
  87. /squirrels/{package_data → _package_data}/base_project/models/sources.yml +0 -0
  88. /squirrels/{package_data → _package_data}/base_project/seeds/seed_categories.csv +0 -0
  89. /squirrels/{package_data → _package_data}/base_project/seeds/seed_categories.yml +0 -0
  90. /squirrels/{package_data → _package_data}/base_project/seeds/seed_subcategories.csv +0 -0
  91. /squirrels/{package_data → _package_data}/base_project/seeds/seed_subcategories.yml +0 -0
  92. /squirrels/{package_data → _package_data}/base_project/squirrels.yml.j2 +0 -0
  93. /squirrels/{package_data → _package_data}/base_project/tmp/.gitignore +0 -0
  94. {squirrels-0.5.0b2.dist-info → squirrels-0.5.0b4.dist-info}/WHEEL +0 -0
  95. {squirrels-0.5.0b2.dist-info → squirrels-0.5.0b4.dist-info}/entry_points.txt +0 -0
  96. {squirrels-0.5.0b2.dist-info → squirrels-0.5.0b4.dist-info}/licenses/LICENSE +0 -0
@@ -9,13 +9,13 @@ from fastapi import Query
9
9
  from pydantic.fields import Field
10
10
  import polars as pl, re
11
11
 
12
- from . import parameter_options as _po, parameters as p, data_sources as d, _utils as u, _constants as c
12
+ from . import _data_sources as d, _parameter_options as po, _parameters as p, _utils as u, _constants as c
13
13
  from ._exceptions import InvalidInputError
14
14
  from ._auth import BaseUser
15
15
  from ._connection_set import ConnectionSet
16
16
  from ._seeds import Seeds
17
17
 
18
- ParamOptionType = TypeVar("ParamOptionType", bound=_po.ParameterOption)
18
+ ParamOptionType = TypeVar("ParamOptionType", bound=po.ParameterOption)
19
19
 
20
20
 
21
21
  @dataclass
@@ -108,7 +108,7 @@ class ParameterConfig(Generic[ParamOptionType], ParameterConfigBase):
108
108
  pass
109
109
 
110
110
  def _invalid_input_error(self, selection: str, more_details: str = '') -> InvalidInputError:
111
- return InvalidInputError(200, f'Selected value "{selection}" is not valid for parameter "{self.name}". ' + more_details)
111
+ return InvalidInputError(400, "Invalid parameter selection", f'Selected value "{selection}" is not valid for parameter "{self.name}". ' + more_details)
112
112
 
113
113
  @abstractmethod
114
114
  def with_selection(
@@ -129,7 +129,7 @@ class ParameterConfig(Generic[ParamOptionType], ParameterConfigBase):
129
129
 
130
130
 
131
131
  @dataclass
132
- class SelectionParameterConfig(ParameterConfig[_po.SelectParameterOption]):
132
+ class SelectionParameterConfig(ParameterConfig[po.SelectParameterOption]):
133
133
  """
134
134
  Abstract class for select parameter classes (single-select, multi-select, etc)
135
135
  """
@@ -138,7 +138,7 @@ class SelectionParameterConfig(ParameterConfig[_po.SelectParameterOption]):
138
138
 
139
139
  @abstractmethod
140
140
  def __init__(
141
- self, name: str, label: str, all_options: Sequence[_po.SelectParameterOption | dict], *,
141
+ self, name: str, label: str, all_options: Sequence[po.SelectParameterOption | dict], *,
142
142
  description: str = "", user_attribute: str | None = None, parent_name: str | None = None
143
143
  ) -> None:
144
144
  super().__init__(name, label, all_options, description=description, user_attribute=user_attribute, parent_name=parent_name)
@@ -147,16 +147,16 @@ class SelectionParameterConfig(ParameterConfig[_po.SelectParameterOption]):
147
147
 
148
148
  @staticmethod
149
149
  def ParameterOption(*args, **kwargs):
150
- return _po.SelectParameterOption(*args, **kwargs)
150
+ return po.SelectParameterOption(*args, **kwargs)
151
151
 
152
152
  def _add_child_mutate(self, child: ParameterConfigBase):
153
153
  self.children[child.name] = child
154
154
  self.trigger_refresh = True
155
155
 
156
- def _get_options(self, user: BaseUser | None, parent_param: p._SelectionParameter | None) -> Sequence[_po.SelectParameterOption]:
156
+ def _get_options(self, user: BaseUser | None, parent_param: p._SelectionParameter | None) -> Sequence[po.SelectParameterOption]:
157
157
  return tuple(self._get_options_iterator(self.all_options, user, parent_param))
158
158
 
159
- def _get_default_ids_iterator(self, options: Sequence[_po.SelectParameterOption]) -> Iterator[str]:
159
+ def _get_default_ids_iterator(self, options: Sequence[po.SelectParameterOption]) -> Iterator[str]:
160
160
  return (x._identifier for x in options if x._is_default)
161
161
 
162
162
  def copy(self) -> Self:
@@ -175,7 +175,7 @@ class SingleSelectParameterConfig(SelectionParameterConfig):
175
175
  """
176
176
 
177
177
  def __init__(
178
- self, name: str, label: str, all_options: Sequence[_po.SelectParameterOption | dict], *, description: str = "",
178
+ self, name: str, label: str, all_options: Sequence[po.SelectParameterOption | dict], *, description: str = "",
179
179
  user_attribute: str | None = None, parent_name: str | None = None
180
180
  ) -> None:
181
181
  super().__init__(name, label, all_options, description=description, user_attribute=user_attribute, parent_name=parent_name)
@@ -217,7 +217,7 @@ class MultiSelectParameterConfig(SelectionParameterConfig):
217
217
  none_is_all: bool = field(default=True, kw_only=True)
218
218
 
219
219
  def __init__(
220
- self, name: str, label: str, all_options: Sequence[_po.SelectParameterOption | dict], *, description: str = "",
220
+ self, name: str, label: str, all_options: Sequence[po.SelectParameterOption | dict], *, description: str = "",
221
221
  show_select_all: bool = True, order_matters: bool = False, none_is_all: bool = True,
222
222
  user_attribute: str | None = None, parent_name: str | None = None
223
223
  ) -> None:
@@ -268,14 +268,14 @@ class _DateTypeParameterConfig(ParameterConfig[ParamOptionType]):
268
268
 
269
269
 
270
270
  @dataclass
271
- class DateParameterConfig(_DateTypeParameterConfig[_po.DateParameterOption]):
271
+ class DateParameterConfig(_DateTypeParameterConfig[po.DateParameterOption]):
272
272
  """
273
273
  Class to define configurations for date parameter widgets.
274
274
  """
275
- _all_options: Sequence[_po.DateParameterOption] = field(repr=False)
275
+ _all_options: Sequence[po.DateParameterOption] = field(repr=False)
276
276
 
277
277
  def __init__(
278
- self, name: str, label: str, all_options: Sequence[_po.DateParameterOption | dict], *,
278
+ self, name: str, label: str, all_options: Sequence[po.DateParameterOption | dict], *,
279
279
  description: str = "", user_attribute: str | None = None, parent_name: str | None = None
280
280
  ) -> None:
281
281
  super().__init__(name, label, all_options, description=description, user_attribute=user_attribute, parent_name=parent_name)
@@ -286,7 +286,7 @@ class DateParameterConfig(_DateTypeParameterConfig[_po.DateParameterOption]):
286
286
 
287
287
  @staticmethod
288
288
  def ParameterOption(*args, **kwargs):
289
- return _po.DateParameterOption(*args, **kwargs)
289
+ return po.DateParameterOption(*args, **kwargs)
290
290
 
291
291
  @staticmethod
292
292
  def DataSource(*args, **kwargs):
@@ -295,7 +295,7 @@ class DateParameterConfig(_DateTypeParameterConfig[_po.DateParameterOption]):
295
295
  def with_selection(
296
296
  self, selection: str | None, user: BaseUser | None, parent_param: p._SelectionParameter | None
297
297
  ) -> p.DateParameter:
298
- curr_option: _po.DateParameterOption | None = next(self._get_options_iterator(self.all_options, user, parent_param), None)
298
+ curr_option: po.DateParameterOption | None = next(self._get_options_iterator(self.all_options, user, parent_param), None)
299
299
  selected_date = curr_option._default_date if selection is None and curr_option is not None else selection
300
300
  return p.DateParameter(self, curr_option, selected_date)
301
301
 
@@ -307,14 +307,14 @@ class DateParameterConfig(_DateTypeParameterConfig[_po.DateParameterOption]):
307
307
 
308
308
 
309
309
  @dataclass
310
- class DateRangeParameterConfig(_DateTypeParameterConfig[_po.DateRangeParameterOption]):
310
+ class DateRangeParameterConfig(_DateTypeParameterConfig[po.DateRangeParameterOption]):
311
311
  """
312
312
  Class to define configurations for date range parameter widgets.
313
313
  """
314
- _all_options: Sequence[_po.DateRangeParameterOption] = field(repr=False)
314
+ _all_options: Sequence[po.DateRangeParameterOption] = field(repr=False)
315
315
 
316
316
  def __init__(
317
- self, name: str, label: str, all_options: Sequence[_po.DateRangeParameterOption | dict], *,
317
+ self, name: str, label: str, all_options: Sequence[po.DateRangeParameterOption | dict], *,
318
318
  description: str = "", user_attribute: str | None = None, parent_name: str | None = None
319
319
  ) -> None:
320
320
  super().__init__(name, label, all_options, description=description, user_attribute=user_attribute, parent_name=parent_name)
@@ -325,7 +325,7 @@ class DateRangeParameterConfig(_DateTypeParameterConfig[_po.DateRangeParameterOp
325
325
 
326
326
  @staticmethod
327
327
  def ParameterOption(*args, **kwargs):
328
- return _po.DateRangeParameterOption(*args, **kwargs)
328
+ return po.DateRangeParameterOption(*args, **kwargs)
329
329
 
330
330
  @staticmethod
331
331
  def DataSource(*args, **kwargs):
@@ -334,7 +334,7 @@ class DateRangeParameterConfig(_DateTypeParameterConfig[_po.DateRangeParameterOp
334
334
  def with_selection(
335
335
  self, selection: str | None, user: BaseUser | None, parent_param: p._SelectionParameter | None
336
336
  ) -> p.DateRangeParameter:
337
- curr_option: _po.DateRangeParameterOption | None = next(self._get_options_iterator(self.all_options, user, parent_param), None)
337
+ curr_option: po.DateRangeParameterOption | None = next(self._get_options_iterator(self.all_options, user, parent_param), None)
338
338
  if selection is None:
339
339
  if curr_option is not None:
340
340
  selected_start_date = curr_option._default_start_date
@@ -370,14 +370,14 @@ class _NumericParameterConfig(ParameterConfig[ParamOptionType]):
370
370
 
371
371
 
372
372
  @dataclass
373
- class NumberParameterConfig(_NumericParameterConfig[_po.NumberParameterOption]):
373
+ class NumberParameterConfig(_NumericParameterConfig[po.NumberParameterOption]):
374
374
  """
375
375
  Class to define configurations for number parameter widgets.
376
376
  """
377
- _all_options: Sequence[_po.NumberParameterOption] = field(repr=False)
377
+ _all_options: Sequence[po.NumberParameterOption] = field(repr=False)
378
378
 
379
379
  def __init__(
380
- self, name: str, label: str, all_options: Sequence[_po.NumberParameterOption | dict], *,
380
+ self, name: str, label: str, all_options: Sequence[po.NumberParameterOption | dict], *,
381
381
  description: str = "", user_attribute: str | None = None, parent_name: str | None = None
382
382
  ) -> None:
383
383
  super().__init__(name, label, all_options, description=description, user_attribute=user_attribute, parent_name=parent_name)
@@ -388,7 +388,7 @@ class NumberParameterConfig(_NumericParameterConfig[_po.NumberParameterOption]):
388
388
 
389
389
  @staticmethod
390
390
  def ParameterOption(*args, **kwargs):
391
- return _po.NumberParameterOption(*args, **kwargs)
391
+ return po.NumberParameterOption(*args, **kwargs)
392
392
 
393
393
  @staticmethod
394
394
  def DataSource(*args, **kwargs):
@@ -397,7 +397,7 @@ class NumberParameterConfig(_NumericParameterConfig[_po.NumberParameterOption]):
397
397
  def with_selection(
398
398
  self, selection: str | None, user: BaseUser | None, parent_param: p._SelectionParameter | None
399
399
  ) -> p.NumberParameter:
400
- curr_option: _po.NumberParameterOption | None = next(self._get_options_iterator(self.all_options, user, parent_param), None)
400
+ curr_option: po.NumberParameterOption | None = next(self._get_options_iterator(self.all_options, user, parent_param), None)
401
401
  selected_value = curr_option._default_value if selection is None and curr_option is not None else selection
402
402
  return p.NumberParameter(self, curr_option, selected_value)
403
403
 
@@ -409,14 +409,14 @@ class NumberParameterConfig(_NumericParameterConfig[_po.NumberParameterOption]):
409
409
 
410
410
 
411
411
  @dataclass
412
- class NumberRangeParameterConfig(_NumericParameterConfig[_po.NumberRangeParameterOption]):
412
+ class NumberRangeParameterConfig(_NumericParameterConfig[po.NumberRangeParameterOption]):
413
413
  """
414
414
  Class to define configurations for number range parameter widgets.
415
415
  """
416
- _all_options: Sequence[_po.NumberRangeParameterOption] = field(repr=False)
416
+ _all_options: Sequence[po.NumberRangeParameterOption] = field(repr=False)
417
417
 
418
418
  def __init__(
419
- self, name: str, label: str, all_options: Sequence[_po.NumberRangeParameterOption | dict], *,
419
+ self, name: str, label: str, all_options: Sequence[po.NumberRangeParameterOption | dict], *,
420
420
  description: str = "", user_attribute: str | None = None, parent_name: str | None = None
421
421
  ) -> None:
422
422
  super().__init__(name, label, all_options, description=description, user_attribute=user_attribute, parent_name=parent_name)
@@ -427,7 +427,7 @@ class NumberRangeParameterConfig(_NumericParameterConfig[_po.NumberRangeParamete
427
427
 
428
428
  @staticmethod
429
429
  def ParameterOption(*args, **kwargs):
430
- return _po.NumberRangeParameterOption(*args, **kwargs)
430
+ return po.NumberRangeParameterOption(*args, **kwargs)
431
431
 
432
432
  @staticmethod
433
433
  def DataSource(*args, **kwargs):
@@ -436,7 +436,7 @@ class NumberRangeParameterConfig(_NumericParameterConfig[_po.NumberRangeParamete
436
436
  def with_selection(
437
437
  self, selection: str | None, user: BaseUser | None, parent_param: p._SelectionParameter | None
438
438
  ) -> p.NumberRangeParameter:
439
- curr_option: _po.NumberRangeParameterOption | None = next(self._get_options_iterator(self.all_options, user, parent_param), None)
439
+ curr_option: po.NumberRangeParameterOption | None = next(self._get_options_iterator(self.all_options, user, parent_param), None)
440
440
  if selection is None:
441
441
  if curr_option is not None:
442
442
  selected_lower_value = curr_option._default_lower_value
@@ -458,15 +458,15 @@ class NumberRangeParameterConfig(_NumericParameterConfig[_po.NumberRangeParamete
458
458
 
459
459
 
460
460
  @dataclass
461
- class TextParameterConfig(ParameterConfig[_po.TextParameterOption]):
461
+ class TextParameterConfig(ParameterConfig[po.TextParameterOption]):
462
462
  """
463
463
  Class to define configurations for text parameter widgets.
464
464
  """
465
- _all_options: Sequence[_po.TextParameterOption] = field(repr=False)
465
+ _all_options: Sequence[po.TextParameterOption] = field(repr=False)
466
466
  input_type: str
467
467
 
468
468
  def __init__(
469
- self, name: str, label: str, all_options: Sequence[_po.TextParameterOption | dict], *, description: str = "",
469
+ self, name: str, label: str, all_options: Sequence[po.TextParameterOption | dict], *, description: str = "",
470
470
  input_type: str = "text", user_attribute: str | None = None, parent_name: str | None = None
471
471
  ) -> None:
472
472
  super().__init__(name, label, all_options, description=description, user_attribute=user_attribute, parent_name=parent_name)
@@ -517,7 +517,7 @@ class TextParameterConfig(ParameterConfig[_po.TextParameterOption]):
517
517
 
518
518
  @staticmethod
519
519
  def ParameterOption(*args, **kwargs):
520
- return _po.TextParameterOption(*args, **kwargs)
520
+ return po.TextParameterOption(*args, **kwargs)
521
521
 
522
522
  @staticmethod
523
523
  def DataSource(*args, **kwargs):
@@ -526,7 +526,7 @@ class TextParameterConfig(ParameterConfig[_po.TextParameterOption]):
526
526
  def with_selection(
527
527
  self, selection: str | None, user: BaseUser | None, parent_param: p._SelectionParameter | None
528
528
  ) -> p.TextParameter:
529
- curr_option: _po.TextParameterOption | None = next(self._get_options_iterator(self.all_options, user, parent_param), None)
529
+ curr_option: po.TextParameterOption | None = next(self._get_options_iterator(self.all_options, user, parent_param), None)
530
530
  entered_text = curr_option._default_text if selection is None and curr_option is not None else selection
531
531
  return p.TextParameter(self, curr_option, entered_text)
532
532
 
@@ -0,0 +1,348 @@
1
+ from typing import TypeVar, Iterable, Any
2
+ from dataclasses import dataclass
3
+ from decimal import Decimal, InvalidOperation as InvalidDecimalConversion
4
+ from datetime import datetime, date
5
+ from abc import ABCMeta, abstractmethod
6
+
7
+ from ._utils import ConfigurationError
8
+
9
+ Number = Decimal | int | float | str
10
+ Comparables = TypeVar("Comparables", Decimal, date)
11
+
12
+
13
+ @dataclass
14
+ class ParameterOption(metaclass=ABCMeta):
15
+ """
16
+ Abstract class for parameter options
17
+ """
18
+ _user_groups: frozenset[Any]
19
+ _parent_option_ids: frozenset[str]
20
+
21
+ @abstractmethod
22
+ def __init__(
23
+ self, *, user_groups: Iterable[Any] | str = frozenset(), parent_option_ids: Iterable[str] | str = frozenset(), **kwargs
24
+ ) -> None:
25
+ self._user_groups = frozenset({user_groups} if isinstance(user_groups, str) else user_groups)
26
+ self._parent_option_ids = frozenset({parent_option_ids} if isinstance(parent_option_ids, str) else parent_option_ids)
27
+
28
+ def _validate_lower_upper_values(self, lower_label: str, lower_value: Comparables, upper_label: str, upper_value: Comparables):
29
+ if lower_value > upper_value:
30
+ raise ConfigurationError(f'The {lower_label} "{lower_value}" must be less than or equal to the {upper_label} "{upper_value}"')
31
+
32
+ def _is_valid(self, user_group: Any, selected_parent_option_ids: Iterable[str] | None) -> bool:
33
+ """
34
+ Checks if this option is valid given the selected parent options and user group of user if applicable.
35
+
36
+ Arguments:
37
+ user_group: The value of the user's "user group attribute". Only None when "user_attribute" is not specified
38
+ for the Parameter factory. Note that when user is None but "user_attribute" is specified, an error is thrown
39
+ selected_parent_option_ids: List of selected option ids from the parent parameter. Only None when the Parameter
40
+ object has no parent parameter.
41
+
42
+ Returns:
43
+ True if valid, False otherwise
44
+ """
45
+ if user_group is not None and user_group not in self._user_groups:
46
+ return False
47
+
48
+ if selected_parent_option_ids is not None and self._parent_option_ids.isdisjoint(selected_parent_option_ids):
49
+ return False
50
+
51
+ return True
52
+
53
+
54
+ @dataclass
55
+ class SelectParameterOption(ParameterOption):
56
+ """
57
+ Parameter option for a select parameter
58
+ """
59
+ _identifier: str
60
+ _label: str
61
+ _is_default: bool
62
+ custom_fields: dict[str, Any]
63
+
64
+ def __init__(
65
+ self, id: str, label: str, *, is_default: bool = False, user_groups: Iterable[Any] | str = frozenset(),
66
+ parent_option_ids: Iterable[str] | str = frozenset(), custom_fields: dict[str, Any] = {}, **kwargs
67
+ ) -> None:
68
+ """
69
+ Constructor for SelectParameterOption
70
+
71
+ Arguments:
72
+ identifier: Unique identifier for this option that never changes over time
73
+ label: Human readable label that gets shown as a dropdown option
74
+ is_default: True if this is a default option, False otherwise
75
+ user_groups: The user groups this parameter option would show for if "user_attribute" is specified in the Parameter factory
76
+ parent_option_ids: Set of parent option ids this parameter option would show for if "parent" is specified in the Parameter factory
77
+ custom_fields: Dictionary to associate custom attributes to the parameter option
78
+ **kwargs: Any additional keyword arguments specified (except the ones above) gets included into custom_fields as well
79
+ """
80
+ super().__init__(user_groups=user_groups, parent_option_ids=parent_option_ids)
81
+ self._identifier = id
82
+ self._label = label
83
+ self._is_default = is_default
84
+ self.custom_fields = {
85
+ **kwargs, **custom_fields, **self._to_json_dict()
86
+ }
87
+
88
+ def get_custom_field(self, field: str, *, default_field: str | None = None, default: Any = None, **kwargs) -> Any:
89
+ """
90
+ Get field value from the custom_fields attribute
91
+
92
+ Arguments:
93
+ field: The key to use to fetch the custom field from "custom_fields"
94
+ default_field: If value at "field" key does not exist in "custom_fields", then this is used instead as the field (if not None)
95
+ default: If value at "field" or "default_field" (if not None) key does not exist in "custom_fields", then this value
96
+ is used as default, or throws an error if None
97
+
98
+ Returns:
99
+ The type of the custom field
100
+ """
101
+ if default_field is not None:
102
+ default = self.get_custom_field(default_field, default=default)
103
+
104
+ if default is not None:
105
+ selected_field = self.custom_fields.get(field, default)
106
+ else:
107
+ try:
108
+ selected_field = self.custom_fields[field]
109
+ except KeyError as e:
110
+ raise ConfigurationError(f"Field '{field}' must exist for parameter option {self._to_json_dict()}") from e
111
+
112
+ return selected_field
113
+
114
+ def _to_json_dict(self):
115
+ return {'id': self._identifier, 'label': self._label}
116
+
117
+
118
+ @dataclass
119
+ class _DateTypeParameterOption(ParameterOption):
120
+ """
121
+ Abstract class (or type) for date type parameter options
122
+ """
123
+ _min_date: date | None
124
+ _max_date: date | None
125
+ _date_format: str
126
+
127
+ @abstractmethod
128
+ def __init__(
129
+ self, *, min_date: str | date | None = None, max_date: str | date | None = None, date_format: str = '%Y-%m-%d',
130
+ user_groups: Iterable[Any] | str = frozenset(), parent_option_ids: Iterable[str] | str = frozenset(), **kwargs
131
+ ) -> None:
132
+ super().__init__(user_groups=user_groups, parent_option_ids=parent_option_ids)
133
+ self._date_format = date_format
134
+ self._min_date, self._max_date = None, None # preset for using _validate_date()
135
+ self._min_date = self._validate_date(min_date) if min_date is not None else None
136
+ self._max_date = self._validate_date(max_date) if max_date is not None else None
137
+ if self._min_date is not None and self._max_date is not None:
138
+ self._validate_lower_upper_values("min_date", self._min_date, "max_date", self._max_date)
139
+
140
+ def _validate_date(self, date_str: str | date) -> date:
141
+ try:
142
+ date_obj = datetime.strptime(date_str, self._date_format).date() if isinstance(date_str, str) else date_str
143
+ except ValueError as e:
144
+ raise ConfigurationError(f'Invalid format for date "{date_str}".') from e
145
+
146
+ if self._min_date is not None and date_obj < self._min_date:
147
+ raise ConfigurationError(f'The provided date "{date_obj}" is less than the min date "{self._min_date}"')
148
+ if self._max_date is not None and date_obj > self._max_date:
149
+ raise ConfigurationError(f'The provided date "{date_obj}" is greater than the max date "{self._max_date}"')
150
+
151
+ return date_obj
152
+
153
+
154
+ @dataclass
155
+ class DateParameterOption(_DateTypeParameterOption):
156
+ """
157
+ Parameter option for default dates if it varies based on selection of another parameter
158
+ """
159
+ _default_date: date
160
+
161
+ def __init__(
162
+ self, default_date: str | date, *, min_date: str | date | None = None, max_date: str | date | None = None, date_format: str = '%Y-%m-%d',
163
+ user_groups: Iterable[Any] | str = frozenset(), parent_option_ids: Iterable[str] | str = frozenset(), **kwargs
164
+ ) -> None:
165
+ """
166
+ Constructor for DateParameterOption
167
+
168
+ Arguments:
169
+ default_date: Default date for this option
170
+ min_date: Minimum date for this option
171
+ max_date: Maximum date for this option
172
+ date_format: Format of the default date, default is '%Y-%m-%d'
173
+ user_groups: The user groups this parameter option would show for if "user_attribute" is specified in the Parameter factory
174
+ parent_option_ids: Set of parent option ids this parameter option would show for if "parent" is specified in the Parameter factory
175
+ """
176
+ super().__init__(
177
+ date_format=date_format, min_date=min_date, max_date=max_date, user_groups=user_groups, parent_option_ids=parent_option_ids
178
+ )
179
+ self._default_date = self._validate_date(default_date)
180
+
181
+
182
+ @dataclass
183
+ class DateRangeParameterOption(_DateTypeParameterOption):
184
+ """
185
+ Parameter option for default dates if it varies based on selection of another parameter
186
+ """
187
+ _default_start_date: date
188
+ _default_end_date: date
189
+
190
+ def __init__(
191
+ self, default_start_date: str | date, default_end_date: str | date, *, min_date: str | date | None = None,
192
+ max_date: str | date | None = None, date_format: str = '%Y-%m-%d', user_groups: Iterable[Any] | str = frozenset(),
193
+ parent_option_ids: Iterable[str] | str = frozenset(), **kwargs
194
+ ) -> None:
195
+ """
196
+ Constructor for DateRangeParameterOption
197
+
198
+ Arguments:
199
+ default_start_date: Default start date for this option
200
+ default_end_date: Default end date for this option
201
+ min_date: Minimum date for this option
202
+ max_date: Maximum date for this option
203
+ date_format: Format of the default date, default is '%Y-%m-%d'
204
+ user_groups: The user groups this parameter option would show for if "user_attribute" is specified in the Parameter factory
205
+ parent_option_ids: Set of parent option ids this parameter option would show for if "parent" is specified in the Parameter factory
206
+ """
207
+ super().__init__(
208
+ date_format=date_format, min_date=min_date, max_date=max_date, user_groups=user_groups, parent_option_ids=parent_option_ids
209
+ )
210
+ self._default_start_date = self._validate_date(default_start_date)
211
+ self._default_end_date = self._validate_date(default_end_date)
212
+ self._validate_lower_upper_values("default_start_date", self._default_start_date, "default_end_date", self._default_end_date)
213
+
214
+
215
+ @dataclass
216
+ class _NumericParameterOption(ParameterOption):
217
+ """
218
+ Abstract class (or type) for numeric parameter options
219
+ """
220
+ _min_value: Decimal
221
+ _max_value: Decimal
222
+ _increment: Decimal
223
+
224
+ @abstractmethod
225
+ def __init__(
226
+ self, min_value: Number, max_value: Number, *, increment: Number = 1, user_groups: Iterable[Any] | str = frozenset(),
227
+ parent_option_ids: Iterable[str] | str = frozenset(), **kwargs
228
+ ) -> None:
229
+ super().__init__(user_groups=user_groups, parent_option_ids=parent_option_ids)
230
+ try:
231
+ self._min_value = Decimal(str(min_value))
232
+ self._max_value = Decimal(str(max_value))
233
+ self._increment = Decimal(str(increment))
234
+ except InvalidDecimalConversion as e:
235
+ raise ConfigurationError(f'Could not convert either min, max, or increment to number') from e
236
+
237
+ self._validate_lower_upper_values("min_value", self._min_value, "max_value", self._max_value)
238
+
239
+ if (self._max_value - self._min_value) % self._increment != 0:
240
+ raise ConfigurationError(f'The increment "{self._increment}" must fit evenly between ' +
241
+ f'the min_value "{self._min_value}" and max_value "{self._max_value}"')
242
+
243
+ def __value_in_range(self, value: Decimal) -> bool:
244
+ return self._min_value <= value <= self._max_value
245
+
246
+ def __value_on_increment(self, value: Decimal) -> bool:
247
+ diff = (value - self._min_value)
248
+ return diff >= 0 and diff % self._increment == 0
249
+
250
+ def _validate_value(self, value: Number) -> Decimal:
251
+ try:
252
+ value = Decimal(str(value))
253
+ except InvalidDecimalConversion as e:
254
+ raise ConfigurationError(f'Could not convert "{value}" to number', e)
255
+
256
+ if not self.__value_in_range(value):
257
+ raise ConfigurationError(f'The selected value "{value}" is outside of bounds ' +
258
+ f'"{self._min_value}" and "{self._max_value}".')
259
+ if not self.__value_on_increment(value):
260
+ raise ConfigurationError(f'The difference between selected value "{value}" and lower value ' +
261
+ f'"{self._min_value}" must be a multiple of increment "{self._increment}".')
262
+ return value
263
+
264
+
265
+ @dataclass
266
+ class NumberParameterOption(_NumericParameterOption):
267
+ """
268
+ Parameter option for default numbers if it varies based on selection of another parameter
269
+ """
270
+ _default_value: Decimal
271
+
272
+ def __init__(
273
+ self, min_value: Number, max_value: Number, *, increment: Number = 1, default_value: Number | None = None,
274
+ user_groups: Iterable[Any] | str = frozenset(), parent_option_ids: Iterable[str] | str = frozenset(), **kwargs
275
+ ) -> None:
276
+ """
277
+ Constructor for NumberParameterOption
278
+
279
+ * Note that the "Number" type denotes an int, a Decimal (from decimal module), or a string that can be parsed to Decimal
280
+
281
+ Arguments:
282
+ min_value: Minimum selectable value
283
+ max_value: Maximum selectable value
284
+ increment: Increment of selectable values, and must fit evenly between min_value and max_value
285
+ default_value: Default value for this option, and must be selectable based on min_value, max_value, and increment
286
+ user_groups: The user groups this parameter option would show for if "user_attribute" is specified in the Parameter factory
287
+ parent_option_ids: Set of parent option ids this parameter option would show for if "parent" is specified in the Parameter factory
288
+ """
289
+ super().__init__(min_value, max_value, increment=increment, user_groups=user_groups, parent_option_ids=parent_option_ids)
290
+ self._default_value = self._validate_value(default_value) if default_value is not None else self._min_value
291
+
292
+
293
+ @dataclass
294
+ class NumberRangeParameterOption(_NumericParameterOption):
295
+ """
296
+ Parameter option for default numeric ranges if it varies based on selection of another parameter
297
+ """
298
+ _default_lower_value: Decimal
299
+ _default_upper_value: Decimal
300
+
301
+ def __init__(
302
+ self, min_value: Number, max_value: Number, *, increment: Number = 1, default_lower_value: Number | None = None,
303
+ default_upper_value: Number | None = None, user_groups: Iterable[Any] | str = frozenset(),
304
+ parent_option_ids: Iterable[str] | str = frozenset(), **kwargs
305
+ ) -> None:
306
+ """
307
+ Constructor for NumberRangeParameterOption
308
+
309
+ * Note that the "Number" type denotes an int, a Decimal (from decimal module), or a string that can be parsed to Decimal
310
+
311
+ Arguments:
312
+ min_value: Minimum selectable value
313
+ max_value: Maximum selectable value
314
+ increment: Increment of selectable values, and must fit evenly between min_value and max_value
315
+ default_lower_value: Default lower value for this option, and must be selectable based on min_value, max_value, and increment
316
+ default_upper_value: Default upper value for this option, and must be selectable based on min_value, max_value, and increment.
317
+ Must also be greater than default_lower_value
318
+ user_groups: The user groups this parameter option would show for if "user_attribute" is specified in the Parameter factory
319
+ parent_option_ids: Set of parent option ids this parameter option would show for if "parent" is specified in the Parameter factory
320
+ """
321
+ super().__init__(min_value, max_value, increment=increment, user_groups=user_groups, parent_option_ids=parent_option_ids)
322
+ self._default_lower_value = self._validate_value(default_lower_value) if default_lower_value is not None else self._min_value
323
+ self._default_upper_value = self._validate_value(default_upper_value) if default_upper_value is not None else self._max_value
324
+ self._validate_lower_upper_values("default_lower_value", self._default_lower_value, "default_upper_value", self._default_upper_value)
325
+
326
+
327
+ @dataclass
328
+ class TextParameterOption(ParameterOption):
329
+ """
330
+ Parameter option for default text values if it varies based on selection of another parameter
331
+ """
332
+ _default_text: str
333
+
334
+ def __init__(
335
+ self, *, default_text: str = "", user_groups: Iterable[Any] | str = frozenset(),
336
+ parent_option_ids: Iterable[str] | str = frozenset(), **kwargs
337
+ ) -> None:
338
+ """
339
+ Constructor for TextParameterOption
340
+
341
+ Arguments:
342
+ default_text: Default text for this option
343
+ user_groups: The user groups this parameter option would show for if "user_attribute" is specified in the Parameter factory
344
+ parent_option_ids: Set of parent option ids this parameter option would show for if "parent" is specified in the Parameter factory
345
+ """
346
+ super().__init__(user_groups=user_groups, parent_option_ids=parent_option_ids)
347
+ self._default_text = default_text
348
+