dara-core 1.17.5__py3-none-any.whl → 1.18.0__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 (68) hide show
  1. dara/core/__init__.py +2 -0
  2. dara/core/actions.py +1 -2
  3. dara/core/auth/basic.py +9 -9
  4. dara/core/auth/routes.py +5 -5
  5. dara/core/auth/utils.py +4 -4
  6. dara/core/base_definitions.py +15 -22
  7. dara/core/cli.py +8 -7
  8. dara/core/configuration.py +5 -2
  9. dara/core/css.py +1 -2
  10. dara/core/data_utils.py +2 -2
  11. dara/core/defaults.py +4 -7
  12. dara/core/definitions.py +6 -9
  13. dara/core/http.py +7 -3
  14. dara/core/interactivity/actions.py +28 -30
  15. dara/core/interactivity/any_data_variable.py +6 -5
  16. dara/core/interactivity/any_variable.py +4 -7
  17. dara/core/interactivity/data_variable.py +1 -1
  18. dara/core/interactivity/derived_data_variable.py +7 -6
  19. dara/core/interactivity/derived_variable.py +93 -33
  20. dara/core/interactivity/filtering.py +19 -27
  21. dara/core/interactivity/plain_variable.py +3 -2
  22. dara/core/interactivity/switch_variable.py +4 -4
  23. dara/core/internal/cache_store/base_impl.py +2 -1
  24. dara/core/internal/cache_store/cache_store.py +17 -5
  25. dara/core/internal/cache_store/keep_all.py +4 -1
  26. dara/core/internal/cache_store/lru.py +5 -1
  27. dara/core/internal/cache_store/ttl.py +4 -1
  28. dara/core/internal/cgroup.py +1 -1
  29. dara/core/internal/dependency_resolution.py +46 -10
  30. dara/core/internal/devtools.py +2 -2
  31. dara/core/internal/download.py +4 -3
  32. dara/core/internal/encoder_registry.py +7 -7
  33. dara/core/internal/execute_action.py +4 -10
  34. dara/core/internal/hashing.py +1 -3
  35. dara/core/internal/import_discovery.py +3 -4
  36. dara/core/internal/normalization.py +9 -13
  37. dara/core/internal/pandas_utils.py +3 -3
  38. dara/core/internal/pool/task_pool.py +16 -10
  39. dara/core/internal/pool/utils.py +5 -7
  40. dara/core/internal/pool/worker.py +3 -2
  41. dara/core/internal/port_utils.py +1 -1
  42. dara/core/internal/registries.py +9 -4
  43. dara/core/internal/registry.py +3 -1
  44. dara/core/internal/registry_lookup.py +7 -3
  45. dara/core/internal/routing.py +77 -44
  46. dara/core/internal/scheduler.py +13 -8
  47. dara/core/internal/settings.py +2 -2
  48. dara/core/internal/tasks.py +8 -14
  49. dara/core/internal/utils.py +11 -10
  50. dara/core/internal/websocket.py +18 -19
  51. dara/core/js_tooling/js_utils.py +23 -24
  52. dara/core/logging.py +3 -6
  53. dara/core/main.py +14 -11
  54. dara/core/metrics/cache.py +1 -1
  55. dara/core/metrics/utils.py +3 -3
  56. dara/core/persistence.py +1 -1
  57. dara/core/umd/dara.core.umd.js +149 -128
  58. dara/core/visual/components/__init__.py +2 -2
  59. dara/core/visual/components/fallback.py +3 -3
  60. dara/core/visual/css/__init__.py +30 -31
  61. dara/core/visual/dynamic_component.py +10 -11
  62. dara/core/visual/progress_updater.py +4 -3
  63. {dara_core-1.17.5.dist-info → dara_core-1.18.0.dist-info}/METADATA +10 -10
  64. dara_core-1.18.0.dist-info/RECORD +114 -0
  65. dara_core-1.17.5.dist-info/RECORD +0 -114
  66. {dara_core-1.17.5.dist-info → dara_core-1.18.0.dist-info}/LICENSE +0 -0
  67. {dara_core-1.17.5.dist-info → dara_core-1.18.0.dist-info}/WHEEL +0 -0
  68. {dara_core-1.17.5.dist-info → dara_core-1.18.0.dist-info}/entry_points.txt +0 -0
@@ -18,7 +18,8 @@ limitations under the License.
18
18
  import abc
19
19
  import io
20
20
  import os
21
- from typing import Any, Awaitable, Callable, Literal, Optional, TypedDict, Union, cast
21
+ from collections.abc import Awaitable
22
+ from typing import Any, Callable, Literal, Optional, TypedDict, Union, cast
22
23
 
23
24
  import pandas
24
25
  from fastapi import UploadFile
@@ -105,8 +106,8 @@ async def upload(data: UploadFile, data_uid: Optional[str] = None, resolver_id:
105
106
  if data_uid is not None:
106
107
  try:
107
108
  variable = await registry_mgr.get(data_variable_registry, data_uid)
108
- except KeyError:
109
- raise ValueError(f'Data Variable {data_uid} does not exist')
109
+ except KeyError as e:
110
+ raise ValueError(f'Data Variable {data_uid} does not exist') from e
110
111
 
111
112
  if variable.type == 'derived':
112
113
  raise ValueError('Cannot upload data to DerivedDataVariable')
@@ -126,12 +127,12 @@ async def upload(data: UploadFile, data_uid: Optional[str] = None, resolver_id:
126
127
  elif file_type == '.xlsx':
127
128
  file_object_xlsx = io.BytesIO(content)
128
129
  content = pandas.read_excel(file_object_xlsx, index_col=None)
129
- content.columns = content.columns.str.replace('Unnamed: *', 'column_', regex=True) # type: ignore
130
+ content.columns = content.columns.str.replace('Unnamed: *', 'column_', regex=True) # type: ignore
130
131
  else:
131
132
  # default to csv
132
133
  file_object_csv = io.StringIO(content.decode('utf-8'))
133
134
  content = pandas.read_csv(file_object_csv, index_col=0)
134
- content.columns = content.columns.str.replace('Unnamed: *', 'column_', regex=True) # type: ignore
135
+ content.columns = content.columns.str.replace('Unnamed: *', 'column_', regex=True) # type: ignore
135
136
 
136
137
  # If a data variable is provided, update it with the new content
137
138
  if variable:
@@ -30,9 +30,8 @@ from fastapi.encoders import jsonable_encoder
30
30
  from pydantic import ConfigDict
31
31
 
32
32
  from dara.core.auth.definitions import SESSION_ID, USER, UserData
33
- from dara.core.base_definitions import BaseTask
33
+ from dara.core.base_definitions import BaseTask, PendingTask
34
34
  from dara.core.base_definitions import DaraBaseModel as BaseModel
35
- from dara.core.base_definitions import PendingTask
36
35
  from dara.core.interactivity.condition import Condition, Operator
37
36
  from dara.core.internal.cache_store import CacheStore
38
37
  from dara.core.internal.tasks import TaskManager
@@ -204,7 +203,6 @@ async def get_current_value(variable: dict, timeout: float = 3, raw: bool = Fals
204
203
 
205
204
  for session, channels in session_channels.items():
206
205
  for ws in channels:
207
-
208
206
  raw_result = raw_results[ws]
209
207
  # Skip values from clients where the variable is not registered
210
208
  if raw_result == NOT_REGISTERED:
@@ -238,12 +236,12 @@ async def get_current_value(variable: dict, timeout: float = 3, raw: bool = Fals
238
236
 
239
237
  # If we're returning multiple values, in Jupyter environments print an explainer
240
238
  try:
241
- from IPython import get_ipython
239
+ from IPython import get_ipython # pyright: ignore[reportMissingImports]
242
240
  except ImportError:
243
241
  pass
244
242
  else:
245
243
  if get_ipython() is not None:
246
- from IPython.display import HTML, display
244
+ from IPython.display import HTML, display # pyright: ignore[reportMissingImports]
247
245
 
248
246
  display(
249
247
  HTML(
@@ -272,7 +270,7 @@ async def get_current_value(variable: dict, timeout: float = 3, raw: bool = Fals
272
270
  return results
273
271
 
274
272
 
275
- class AnyVariable(BaseModel, abc.ABC):
273
+ class AnyVariable(BaseModel, abc.ABC): # noqa: PLW1641 # we override equals to create conditions, otherwise we should define hash
276
274
  """
277
275
  Base class for all variables. Used for typing to specify that any variable can be provided.
278
276
  """
@@ -282,7 +280,6 @@ class AnyVariable(BaseModel, abc.ABC):
282
280
  uid: str
283
281
 
284
282
  def __init__(self, uid: Optional[str] = None, **kwargs) -> None:
285
-
286
283
  new_uid = uid
287
284
  if new_uid is None:
288
285
  new_uid = str(uuid.uuid4())
@@ -312,7 +312,7 @@ class DataVariable(AnyDataVariable):
312
312
  def ser_model(self, nxt: SerializerFunctionWrapHandler) -> dict:
313
313
  parent_dict = nxt(self)
314
314
  if 'data' in parent_dict:
315
- parent_dict.pop('data') # make sure data is not included in the serialised dict
315
+ parent_dict.pop('data') # make sure data is not included in the serialised dict
316
316
  return {**parent_dict, '__typename': 'DataVariable', 'uid': str(parent_dict['uid'])}
317
317
 
318
318
 
@@ -18,7 +18,8 @@ limitations under the License.
18
18
  from __future__ import annotations
19
19
 
20
20
  import asyncio
21
- from typing import Any, Callable, Coroutine, List, Optional, Union, cast
21
+ from collections.abc import Coroutine
22
+ from typing import Any, Callable, List, Optional, Union, cast
22
23
  from uuid import uuid4
23
24
 
24
25
  from pandas import DataFrame
@@ -185,7 +186,7 @@ class DerivedDataVariable(AnyDataVariable, DerivedVariable):
185
186
  store: CacheStore,
186
187
  task_mgr: TaskManager,
187
188
  args: List[Any],
188
- force: bool = False,
189
+ force_key: Optional[str] = None,
189
190
  ) -> DerivedVariableResult:
190
191
  """
191
192
  Update the underlying derived variable.
@@ -201,7 +202,7 @@ class DerivedDataVariable(AnyDataVariable, DerivedVariable):
201
202
  eng_logger.info(
202
203
  f'Derived Data Variable {_uid_short} calling superclass get_value', {'uid': var_entry.uid, 'args': args}
203
204
  )
204
- value = await super().get_value(var_entry, store, task_mgr, args, force)
205
+ value = await super().get_value(var_entry, store, task_mgr, args, force_key)
205
206
 
206
207
  # Pin the value in the store until it's read by get data
207
208
  await asyncio.gather(
@@ -339,8 +340,8 @@ class DerivedDataVariable(AnyDataVariable, DerivedVariable):
339
340
  store: CacheStore,
340
341
  task_mgr: TaskManager,
341
342
  args: List[Any],
342
- force: bool,
343
343
  filters: Optional[Union[FilterQuery, dict]] = None,
344
+ force_key: Optional[str] = None,
344
345
  ):
345
346
  """
346
347
  Helper method to resolve the filtered value of a derived data variable.
@@ -351,11 +352,11 @@ class DerivedDataVariable(AnyDataVariable, DerivedVariable):
351
352
  :param store: the store instance to check for cached values
352
353
  :param task_mgr: task manager instance
353
354
  :param args: the arguments to call the underlying function with
354
- :param force: whether to ignore cache
355
355
  :param filters: the filters to apply to the data
356
+ :param force_key: unique key for forced execution, if provided forces cache bypass
356
357
  :param pagination: the pagination to apply to the data
357
358
  """
358
- dv_result = await cls.get_value(dv_entry, store, task_mgr, args, force)
359
+ dv_result = await cls.get_value(dv_entry, store, task_mgr, args, force_key)
359
360
 
360
361
  # If the intermediate result was a task/metatask, we need to run it
361
362
  # get_data will then pick up the result from the pending task for it
@@ -19,10 +19,10 @@ from __future__ import annotations
19
19
 
20
20
  import json
21
21
  import uuid
22
+ from collections.abc import Awaitable
22
23
  from inspect import Parameter, signature
23
24
  from typing import (
24
25
  Any,
25
- Awaitable,
26
26
  Callable,
27
27
  Generic,
28
28
  List,
@@ -32,6 +32,7 @@ from typing import (
32
32
  cast,
33
33
  )
34
34
 
35
+ from cachetools import LRUCache
35
36
  from pydantic import (
36
37
  ConfigDict,
37
38
  Field,
@@ -63,6 +64,16 @@ from dara.core.metrics import RUNTIME_METRICS_TRACKER
63
64
 
64
65
  VariableType = TypeVar('VariableType')
65
66
 
67
+ # Global set to track force keys that have been encountered
68
+ # LRU with 2048 entries should be sufficient to not drop in-progress force keys
69
+ # but also not have to worry about memory leaks
70
+ _force_keys_seen: LRUCache[str, bool] = LRUCache(maxsize=2048)
71
+
72
+ VALUE_MISSING = object()
73
+ """
74
+ Sentinel value to indicate that a value is missing from the cache
75
+ """
76
+
66
77
 
67
78
  class DerivedVariableResult(TypedDict):
68
79
  cache_key: str
@@ -146,7 +157,12 @@ class DerivedVariable(NonDataVariable, Generic[VariableType]):
146
157
  raise RuntimeError('run_as_task is not supported within a Jupyter environment')
147
158
 
148
159
  super().__init__(
149
- cache=cache, uid=uid, variables=variables, polling_interval=polling_interval, deps=deps, nested=nested
160
+ cache=cache,
161
+ uid=uid,
162
+ variables=variables,
163
+ polling_interval=polling_interval,
164
+ deps=deps,
165
+ nested=nested,
150
166
  )
151
167
 
152
168
  # Import the registry of variables and register the function at import
@@ -208,15 +224,17 @@ class DerivedVariable(NonDataVariable, Generic[VariableType]):
208
224
  :param uid: uid of a DerivedVariable
209
225
  :param deps: list of indexes of dependencies
210
226
  """
227
+ from dara.core.internal.dependency_resolution import clean_force_key
228
+
211
229
  key = f'{uid}'
212
230
 
213
231
  filtered_args = [arg for idx, arg in enumerate(args) if idx in deps] if deps is not None else args
214
232
 
215
- for arg in filtered_args:
216
- if isinstance(arg, dict):
217
- key = f'{key}:{json.dumps(arg, sort_keys=True, default=str)}'
218
- else:
219
- key = f'{key}:{arg}'
233
+ for raw_arg in filtered_args:
234
+ # remove force keys from the arg to not cause extra cache misses
235
+ arg = clean_force_key(raw_arg)
236
+
237
+ key = f'{key}:{json.dumps(arg, sort_keys=True, default=str)}' if isinstance(arg, dict) else f'{key}:{arg}'
220
238
  return key
221
239
 
222
240
  @staticmethod
@@ -269,7 +287,8 @@ class DerivedVariable(NonDataVariable, Generic[VariableType]):
269
287
  if not latest_value_registry.has(var_entry.uid):
270
288
  # Keep latest entry per scope (user,session); if cache_type is None, use GLOBAL
271
289
  reg_entry = LatestValueRegistryEntry(
272
- uid=var_entry.uid, cache=Cache.Policy.MostRecent(cache_type=cache_type or Cache.Type.GLOBAL)
290
+ uid=var_entry.uid,
291
+ cache=Cache.Policy.MostRecent(cache_type=cache_type or Cache.Type.GLOBAL),
273
292
  )
274
293
  latest_value_registry.register(var_entry.uid, reg_entry)
275
294
  else:
@@ -285,7 +304,7 @@ class DerivedVariable(NonDataVariable, Generic[VariableType]):
285
304
  store: CacheStore,
286
305
  task_mgr: TaskManager,
287
306
  args: List[Any],
288
- force: bool = False,
307
+ force_key: Optional[str] = None,
289
308
  ) -> DerivedVariableResult:
290
309
  """
291
310
  Get the value of this DerivedVariable. This method will check the main app store for an appropriate response
@@ -296,7 +315,7 @@ class DerivedVariable(NonDataVariable, Generic[VariableType]):
296
315
  :param store: the store instance to check for cached values
297
316
  :param task_mgr: task manager instance
298
317
  :param args: the arguments to call the underlying function with
299
- :param force: whether to ignore cache
318
+ :param force_key: unique key for forced execution, if provided forces cache bypass
300
319
  """
301
320
  assert var_entry.func is not None, 'DerivedVariable function is not defined'
302
321
 
@@ -318,30 +337,32 @@ class DerivedVariable(NonDataVariable, Generic[VariableType]):
318
337
  values = []
319
338
 
320
339
  # dynamic import due to circular import
321
- from dara.core.internal.dependency_resolution import (
322
- is_resolved_derived_variable,
323
- resolve_dependency,
324
- )
340
+ from dara.core.internal.dependency_resolution import resolve_dependency
325
341
 
326
- eng_logger.info(f'Derived Variable {_uid_short} get_value', {'uid': var_entry.uid, 'args': args})
342
+ eng_logger.info(
343
+ f'Derived Variable {_uid_short} get_value',
344
+ {'uid': var_entry.uid, 'args': args},
345
+ )
327
346
 
328
347
  for val in args:
329
- # Don't force nested DVs
330
- if is_resolved_derived_variable(val):
331
- val['force'] = False
332
-
333
348
  var_value = await resolve_dependency(val, store, task_mgr)
334
349
  values.append(var_value)
335
350
 
336
351
  eng_logger.debug(
337
- f'DerivedVariable {_uid_short}', 'resolved arguments', {'values': values, 'uid': var_entry.uid}
352
+ f'DerivedVariable {_uid_short}',
353
+ 'resolved arguments',
354
+ {'values': values, 'uid': var_entry.uid},
338
355
  )
339
356
 
340
357
  # Loop over the passed arguments and if the expected type is a BaseModel and arg is a dict then convert the dict
341
358
  # to an instance of the BaseModel class.
342
359
  parsed_args = DerivedVariable._restore_pydantic_models(var_entry.func, *values)
343
360
 
344
- dev_logger.debug(f'DerivedVariable {_uid_short}', 'executing', {'args': parsed_args, 'uid': var_entry.uid})
361
+ dev_logger.debug(
362
+ f'DerivedVariable {_uid_short}',
363
+ 'executing',
364
+ {'args': parsed_args, 'uid': var_entry.uid},
365
+ )
345
366
 
346
367
  # Check if there are any Tasks to be run in the args
347
368
  has_tasks = any(isinstance(arg, BaseTask) for arg in parsed_args)
@@ -351,6 +372,24 @@ class DerivedVariable(NonDataVariable, Generic[VariableType]):
351
372
 
352
373
  cache_type = var_entry.cache
353
374
 
375
+ # Handle force key tracking to prevent double execution
376
+ effective_force = force_key is not None
377
+ if force_key is not None:
378
+ if force_key in _force_keys_seen:
379
+ # This force key has been seen before, don't force again
380
+ effective_force = False
381
+ eng_logger.debug(
382
+ f'DerivedVariable {_uid_short} force key already seen, using cached value',
383
+ extra={'uid': var_entry.uid, 'force_key': force_key},
384
+ )
385
+ else:
386
+ # First time seeing this force key, add it to the set
387
+ _force_keys_seen[force_key] = True
388
+ eng_logger.debug(
389
+ f'DerivedVariable {_uid_short} new force key, will force recalculation',
390
+ extra={'uid': var_entry.uid, 'force_key': force_key},
391
+ )
392
+
354
393
  # If deps is not None, force session use
355
394
  # Note: this is temporarily commented out as no tests were broken by removing it;
356
395
  # once we find what scenario this fixes, we should add a test to cover that scenario and move this snippet
@@ -364,18 +403,33 @@ class DerivedVariable(NonDataVariable, Generic[VariableType]):
364
403
  {'uid': var_entry.uid},
365
404
  )
366
405
 
406
+ # Start with a sentinel value to indicate that the value is missing
407
+ # from cache, this lets us distinguish between a cache miss and a
408
+ # value that is None
409
+ value = VALUE_MISSING
410
+
367
411
  ignore_cache = (
368
412
  var_entry.cache is None
369
413
  or var_entry.polling_interval
370
414
  or DerivedVariable.check_polling(var_entry.variables)
415
+ or effective_force
371
416
  )
372
- value = await store.get(var_entry, key=cache_key) if not ignore_cache else None
373
-
374
- eng_logger.debug(
375
- f'DerivedVariable {_uid_short}',
376
- 'retrieved value from cache',
377
- {'uid': var_entry.uid, 'cached_value': value},
378
- )
417
+ if not ignore_cache:
418
+ try:
419
+ value = await store.get(var_entry, key=cache_key, raise_for_missing=True)
420
+ eng_logger.debug(
421
+ f'DerivedVariable {_uid_short}',
422
+ 'retrieved value from cache',
423
+ {'uid': var_entry.uid, 'cached_value': value},
424
+ )
425
+ except KeyError:
426
+ eng_logger.debug(
427
+ f'DerivedVariable {_uid_short}',
428
+ 'no value found in cache',
429
+ {'uid': var_entry.uid},
430
+ )
431
+ # key error means no entry found;
432
+ # this lets us distinguish from a None value stored and not found
379
433
 
380
434
  # If it's a PendingTask then return that task so it can be awaited later by a MetaTask
381
435
  if isinstance(value, PendingTask):
@@ -392,11 +446,13 @@ class DerivedVariable(NonDataVariable, Generic[VariableType]):
392
446
  f'DerivedVariable {_uid_short} waiting for pending value',
393
447
  {'uid': var_entry.uid, 'pending_value': value},
394
448
  )
395
- return {'cache_key': cache_key, 'value': await store.get_or_wait(var_entry, key=cache_key)}
449
+ return {
450
+ 'cache_key': cache_key,
451
+ 'value': await store.get_or_wait(var_entry, key=cache_key),
452
+ }
396
453
 
397
- # If there is a value that is not pending then we have the result so return it
398
- # If force is True, don't return even if value is found and recalculate
399
- if not force and value is not None:
454
+ # We retrieved an actual value from the cache, return it
455
+ if not ignore_cache and value is not VALUE_MISSING:
400
456
  eng_logger.info(
401
457
  f'DerivedVariable {_uid_short} returning cached value directly',
402
458
  {'uid': var_entry.uid, 'cached_value': value},
@@ -492,7 +548,11 @@ class DerivedVariable(NonDataVariable, Generic[VariableType]):
492
548
  @model_serializer(mode='wrap')
493
549
  def ser_model(self, nxt: SerializerFunctionWrapHandler) -> dict:
494
550
  parent_dict = nxt(self)
495
- return {**parent_dict, '__typename': 'DerivedVariable', 'uid': str(parent_dict['uid'])}
551
+ return {
552
+ **parent_dict,
553
+ '__typename': 'DerivedVariable',
554
+ 'uid': str(parent_dict['uid']),
555
+ }
496
556
 
497
557
 
498
558
  class DerivedVariableRegistryEntry(CachedRegistryEntry):
@@ -20,10 +20,10 @@ from __future__ import annotations
20
20
  import re
21
21
  from datetime import datetime, timezone
22
22
  from enum import Enum
23
- from typing import Any, List, Optional, Tuple, Union
23
+ from typing import Any, List, Optional, Tuple, Union, cast
24
24
 
25
25
  import numpy
26
- from pandas import DataFrame, Series # pylint: disable=unused-import
26
+ from pandas import DataFrame, Series # noqa: F401
27
27
 
28
28
  from dara.core.base_definitions import DaraBaseModel as BaseModel
29
29
  from dara.core.logging import dev_logger
@@ -157,11 +157,11 @@ def infer_column_type(series: Series) -> ColumnType:
157
157
  return ColumnType.CATEGORICAL
158
158
 
159
159
 
160
- def _filter_to_series(data: DataFrame, column: str, operator: QueryOperator, value: Any) -> Optional['Series[bool]']:
160
+ def _filter_to_series(data: DataFrame, column: str, operator: QueryOperator, value: Any) -> Optional[Series]:
161
161
  """
162
162
  Convert a single filter to a Series[bool] for filtering
163
163
  """
164
- series = data[column]
164
+ series = cast(Series, data[column])
165
165
 
166
166
  # Contains is a special case, we always treat the column as a string
167
167
  if operator == QueryOperator.CONTAINS:
@@ -175,19 +175,15 @@ def _filter_to_series(data: DataFrame, column: str, operator: QueryOperator, val
175
175
  return series.isin(value)
176
176
  # Converts date passed from frontend to the right format to compare with pandas
177
177
  if col_type == ColumnType.DATETIME:
178
- if isinstance(value, List):
179
- value = [parseISO(value[0]), parseISO(value[1])]
180
- else:
181
- value = parseISO(value)
178
+ value = [parseISO(value[0]), parseISO(value[1])] if isinstance(value, List) else parseISO(value)
182
179
  elif col_type == ColumnType.CATEGORICAL:
183
180
  value = str(value)
181
+ elif isinstance(value, List):
182
+ lower_bound = float(value[0]) if '.' in str(value[0]) else int(value[0])
183
+ upper_bound = float(value[1]) if '.' in str(value[1]) else int(value[1])
184
+ value = [lower_bound, upper_bound]
184
185
  else:
185
- if isinstance(value, List):
186
- lower_bound = float(value[0]) if '.' in str(value[0]) else int(value[0])
187
- upper_bound = float(value[1]) if '.' in str(value[1]) else int(value[1])
188
- value = [lower_bound, upper_bound]
189
- else:
190
- value = float(value) if '.' in str(value) else int(value)
186
+ value = float(value) if '.' in str(value) else int(value)
191
187
 
192
188
  if operator == QueryOperator.GT:
193
189
  return series > value
@@ -208,12 +204,14 @@ def _filter_to_series(data: DataFrame, column: str, operator: QueryOperator, val
208
204
  return None
209
205
 
210
206
 
211
- def _resolve_filter_query(data: DataFrame, query: FilterQuery) -> 'Optional[Series[bool]]':
207
+ def _resolve_filter_query(data: DataFrame, query: FilterQuery) -> Optional[Series]:
212
208
  """
213
209
  Resolve a FilterQuery to a Series[bool] for filtering. Strips the internal column index from the query.
214
210
  """
215
211
  if isinstance(query, ValueQuery):
216
- return _filter_to_series(data, re.sub(COLUMN_PREFIX_REGEX, '', query.column, 1), query.operator, query.value)
212
+ return _filter_to_series(
213
+ data, re.sub(COLUMN_PREFIX_REGEX, repl='', string=query.column, count=1), query.operator, query.value
214
+ )
217
215
  elif isinstance(query, ClauseQuery):
218
216
  filters = None
219
217
 
@@ -222,15 +220,9 @@ def _resolve_filter_query(data: DataFrame, query: FilterQuery) -> 'Optional[Seri
222
220
 
223
221
  if resolved_clause is not None:
224
222
  if query.combinator == QueryCombinator.AND:
225
- if filters is None:
226
- filters = resolved_clause
227
- else:
228
- filters = filters & resolved_clause
223
+ filters = resolved_clause if filters is None else filters & resolved_clause
229
224
  elif query.combinator == QueryCombinator.OR:
230
- if filters is None:
231
- filters = resolved_clause
232
- else:
233
- filters = filters | resolved_clause
225
+ filters = resolved_clause if filters is None else filters | resolved_clause
234
226
  else:
235
227
  raise ValueError(f'Unknown combinator {query.combinator}')
236
228
 
@@ -262,7 +254,7 @@ def apply_filters(
262
254
  if pagination is not None:
263
255
  # ON FETCHING SPECIFIC ROW
264
256
  if pagination.index is not None:
265
- return data[int(pagination.index) : int(pagination.index) + 1], total_count
257
+ return cast(DataFrame, data[int(pagination.index) : int(pagination.index) + 1]), total_count
266
258
 
267
259
  # SORT
268
260
  if pagination.orderBy is not None:
@@ -278,7 +270,7 @@ def apply_filters(
278
270
  if col == 'index':
279
271
  new_data = new_data.sort_index(ascending=ascending, inplace=False)
280
272
  else:
281
- new_data = new_data.sort_values(by=col, ascending=ascending, inplace=False)
273
+ new_data = new_data.sort_values(by=col, ascending=ascending, inplace=False) # type: ignore
282
274
 
283
275
  # PAGINATE
284
276
  start_index = pagination.offset if pagination.offset is not None else 0
@@ -286,4 +278,4 @@ def apply_filters(
286
278
 
287
279
  new_data = new_data.iloc[start_index:stop_index]
288
280
 
289
- return new_data, total_count
281
+ return cast(DataFrame, new_data), total_count
@@ -42,6 +42,7 @@ VARIABLE_INIT_OVERRIDE = ContextVar[Optional[Callable[[dict], dict]]]('VARIABLE_
42
42
  VariableType = TypeVar('VariableType')
43
43
  PersistenceStoreType_co = TypeVar('PersistenceStoreType_co', bound=PersistenceStore, covariant=True)
44
44
 
45
+
45
46
  # TODO: once Python supports a default value for a generic type properly we can make PersistenceStoreType a second generic param
46
47
  class Variable(NonDataVariable, Generic[VariableType]):
47
48
  """
@@ -84,7 +85,7 @@ class Variable(NonDataVariable, Generic[VariableType]):
84
85
  # TODO: this is temporary, persist_value will eventually become a type of store
85
86
  raise ValueError('Cannot provide a Variable with both a store and persist_value set to True')
86
87
 
87
- super().__init__(**kwargs) # type: ignore
88
+ super().__init__(**kwargs) # type: ignore
88
89
 
89
90
  if self.store:
90
91
  call_async(self.store.init, self)
@@ -257,7 +258,7 @@ class Variable(NonDataVariable, Generic[VariableType]):
257
258
  'Cannot create a Variable from a DerivedDataVariable, only standard DerivedVariables are allowed'
258
259
  )
259
260
 
260
- return cls(default=other) # type: ignore
261
+ return cls(default=other) # type: ignore
261
262
 
262
263
  @model_serializer(mode='wrap')
263
264
  def ser_model(self, nxt: SerializerFunctionWrapHandler) -> dict:
@@ -216,8 +216,8 @@ class SwitchVariable(NonDataVariable):
216
216
  Key Serialization:
217
217
  When using mappings with SwitchVariable, be aware that JavaScript object keys
218
218
  are always strings. The system automatically converts lookup keys to strings:
219
- - Python: {True: 'admin', False: 'user'}
220
- - JavaScript: {"true": "admin", "false": "user"}
219
+ - Python: `{True: 'admin', False: 'user'}`
220
+ - JavaScript: `{"true": "admin", "false": "user"}`
221
221
  - Boolean values are converted to lowercase strings ("true"/"false")
222
222
  - Other values use standard string conversion to match JavaScript's String() behavior
223
223
  """
@@ -274,7 +274,7 @@ class SwitchVariable(NonDataVariable):
274
274
  true_value: Union[Any, NonDataVariable],
275
275
  false_value: Union[Any, NonDataVariable],
276
276
  uid: Optional[str] = None,
277
- ) -> 'SwitchVariable':
277
+ ) -> SwitchVariable:
278
278
  """
279
279
  Create a SwitchVariable for boolean conditions.
280
280
 
@@ -350,7 +350,7 @@ class SwitchVariable(NonDataVariable):
350
350
  mapping: Union[Dict[Any, Any], NonDataVariable],
351
351
  default: Optional[Union[Any, NonDataVariable]] = None,
352
352
  uid: Optional[str] = None,
353
- ) -> 'SwitchVariable':
353
+ ) -> SwitchVariable:
354
354
  """
355
355
  Create a SwitchVariable with a custom mapping.
356
356
 
@@ -19,12 +19,13 @@ class CacheStoreImpl(abc.ABC, Generic[PolicyT]):
19
19
  """
20
20
 
21
21
  @abc.abstractmethod
22
- async def get(self, key: str, unpin: bool = False) -> Any:
22
+ async def get(self, key: str, unpin: bool = False, raise_for_missing: bool = False) -> Any:
23
23
  """
24
24
  Retrieve an entry from the cache.
25
25
 
26
26
  :param key: The key of the entry to retrieve.
27
27
  :param unpin: If true, the entry will be unpinned if it is pinned.
28
+ :param raise_for_missing: If true, an exception will be raised if the entry is not found
28
29
  """
29
30
 
30
31
  @abc.abstractmethod
@@ -23,7 +23,7 @@ def cache_impl_for_policy(policy: PolicyT) -> CacheStoreImpl[PolicyT]:
23
23
  """
24
24
  impl: Optional[CacheStoreImpl] = None
25
25
 
26
- if isinstance(policy, LruCachePolicy) or isinstance(policy, MostRecentCachePolicy):
26
+ if isinstance(policy, (LruCachePolicy, MostRecentCachePolicy)):
27
27
  impl = LRUCache(policy)
28
28
  elif isinstance(policy, TTLCachePolicy):
29
29
  impl = TTLCache(policy)
@@ -62,21 +62,24 @@ class CacheScopeStore(Generic[PolicyT]):
62
62
 
63
63
  return await cache.delete(key)
64
64
 
65
- async def get(self, key: str, unpin: bool = False) -> Optional[Any]:
65
+ async def get(self, key: str, unpin: bool = False, raise_for_missing: bool = False) -> Optional[Any]:
66
66
  """
67
67
  Retrieve an entry from the cache.
68
68
 
69
69
  :param key: The key of the entry to retrieve.
70
70
  :param unpin: If true, the entry will be unpinned if it is pinned.
71
+ :param raise_for_missing: If true, an exception will be raised if the entry is not found
71
72
  """
72
73
  scope = get_cache_scope(self.policy.cache_type)
73
74
  cache = self.caches.get(scope)
74
75
 
75
76
  # No cache for this scope yet
76
77
  if cache is None:
78
+ if raise_for_missing:
79
+ raise KeyError(f'No cache found for {scope}')
77
80
  return None
78
81
 
79
- return await cache.get(key, unpin=unpin)
82
+ return await cache.get(key, unpin=unpin, raise_for_missing=raise_for_missing)
80
83
 
81
84
  async def set(self, key: str, value: Any, pin: bool = False):
82
85
  """
@@ -150,21 +153,30 @@ class CacheStore:
150
153
 
151
154
  return prev_entry
152
155
 
153
- async def get(self, registry_entry: CachedRegistryEntry, key: str, unpin: bool = False) -> Optional[Any]:
156
+ async def get(
157
+ self,
158
+ registry_entry: CachedRegistryEntry,
159
+ key: str,
160
+ unpin: bool = False,
161
+ raise_for_missing: bool = False,
162
+ ) -> Optional[Any]:
154
163
  """
155
164
  Retrieve an entry from the cache for the given registry entry and cache key.
156
165
 
157
166
  :param registry_entry: The registry entry to retrieve the value for.
158
167
  :param key: The key of the entry to retrieve.
159
168
  :param unpin: If true, the entry will be unpinned if it is pinned.
169
+ :param raise_for_missing: If true, an exception will be raised if the entry is not found
160
170
  """
161
171
  registry_store = self.registry_stores.get(registry_entry.to_store_key())
162
172
 
163
173
  # No store for this entry yet
164
174
  if registry_store is None:
175
+ if raise_for_missing:
176
+ raise KeyError(f'No cache store found for {registry_entry.to_store_key()}')
165
177
  return None
166
178
 
167
- return await registry_store.get(key, unpin=unpin)
179
+ return await registry_store.get(key, unpin=unpin, raise_for_missing=raise_for_missing)
168
180
 
169
181
  async def get_or_wait(self, registry_entry: CachedRegistryEntry, key: str):
170
182
  """
@@ -42,18 +42,21 @@ class KeepAllCache(CacheStoreImpl[KeepAllCachePolicy]):
42
42
  del self.cache[key]
43
43
  return entry
44
44
 
45
- async def get(self, key: str, unpin: bool = False) -> Optional[Any]:
45
+ async def get(self, key: str, unpin: bool = False, raise_for_missing: bool = False) -> Optional[Any]:
46
46
  """
47
47
  Retrieve a value from the cache.
48
48
 
49
49
  :param key: The key of the value to retrieve.
50
50
  :param unpin: This parameter is ignored in KeepAllCache as entries are never evicted.
51
+ :param raise_for_missing: If true, an exception will be raised if the entry is not found
51
52
  :return: The value associated with the key, or None if the key is not in the cache.
52
53
  """
53
54
  async with self.lock:
54
55
  entry = self.cache.get(key)
55
56
 
56
57
  if entry is None:
58
+ if raise_for_missing:
59
+ raise KeyError(f'No cache entry found for {key}')
57
60
  return None
58
61
 
59
62
  if unpin: