dara-core 1.17.6__py3-none-any.whl → 1.18.1__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.
- dara/core/__init__.py +2 -0
- dara/core/actions.py +1 -2
- dara/core/auth/basic.py +9 -9
- dara/core/auth/routes.py +5 -5
- dara/core/auth/utils.py +4 -4
- dara/core/base_definitions.py +15 -22
- dara/core/cli.py +8 -7
- dara/core/configuration.py +5 -2
- dara/core/css.py +1 -2
- dara/core/data_utils.py +2 -2
- dara/core/defaults.py +4 -7
- dara/core/definitions.py +6 -9
- dara/core/http.py +7 -3
- dara/core/interactivity/actions.py +28 -30
- dara/core/interactivity/any_data_variable.py +6 -5
- dara/core/interactivity/any_variable.py +4 -7
- dara/core/interactivity/data_variable.py +1 -1
- dara/core/interactivity/derived_data_variable.py +7 -6
- dara/core/interactivity/derived_variable.py +93 -33
- dara/core/interactivity/filtering.py +19 -27
- dara/core/interactivity/plain_variable.py +3 -2
- dara/core/interactivity/switch_variable.py +4 -4
- dara/core/internal/cache_store/base_impl.py +2 -1
- dara/core/internal/cache_store/cache_store.py +17 -5
- dara/core/internal/cache_store/keep_all.py +4 -1
- dara/core/internal/cache_store/lru.py +5 -1
- dara/core/internal/cache_store/ttl.py +4 -1
- dara/core/internal/cgroup.py +1 -1
- dara/core/internal/dependency_resolution.py +46 -10
- dara/core/internal/devtools.py +2 -2
- dara/core/internal/download.py +4 -3
- dara/core/internal/encoder_registry.py +7 -7
- dara/core/internal/execute_action.py +4 -10
- dara/core/internal/hashing.py +1 -3
- dara/core/internal/import_discovery.py +3 -4
- dara/core/internal/normalization.py +9 -13
- dara/core/internal/pandas_utils.py +3 -3
- dara/core/internal/pool/task_pool.py +16 -10
- dara/core/internal/pool/utils.py +5 -7
- dara/core/internal/pool/worker.py +3 -2
- dara/core/internal/port_utils.py +1 -1
- dara/core/internal/registries.py +9 -4
- dara/core/internal/registry.py +3 -1
- dara/core/internal/registry_lookup.py +7 -3
- dara/core/internal/routing.py +77 -44
- dara/core/internal/scheduler.py +13 -8
- dara/core/internal/settings.py +2 -2
- dara/core/internal/tasks.py +8 -14
- dara/core/internal/utils.py +11 -10
- dara/core/internal/websocket.py +18 -19
- dara/core/js_tooling/js_utils.py +23 -24
- dara/core/logging.py +3 -6
- dara/core/main.py +14 -11
- dara/core/metrics/cache.py +1 -1
- dara/core/metrics/utils.py +3 -3
- dara/core/persistence.py +1 -1
- dara/core/umd/dara.core.umd.js +146 -128
- dara/core/visual/components/__init__.py +2 -2
- dara/core/visual/components/fallback.py +3 -3
- dara/core/visual/css/__init__.py +30 -31
- dara/core/visual/dynamic_component.py +10 -11
- dara/core/visual/progress_updater.py +4 -3
- {dara_core-1.17.6.dist-info → dara_core-1.18.1.dist-info}/METADATA +11 -10
- dara_core-1.18.1.dist-info/RECORD +114 -0
- dara_core-1.17.6.dist-info/RECORD +0 -114
- {dara_core-1.17.6.dist-info → dara_core-1.18.1.dist-info}/LICENSE +0 -0
- {dara_core-1.17.6.dist-info → dara_core-1.18.1.dist-info}/WHEEL +0 -0
- {dara_core-1.17.6.dist-info → dara_core-1.18.1.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
|
|
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)
|
|
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)
|
|
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')
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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(
|
|
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}',
|
|
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(
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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 {
|
|
449
|
+
return {
|
|
450
|
+
'cache_key': cache_key,
|
|
451
|
+
'value': await store.get_or_wait(var_entry, key=cache_key),
|
|
452
|
+
}
|
|
396
453
|
|
|
397
|
-
#
|
|
398
|
-
|
|
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 {
|
|
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 #
|
|
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[
|
|
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
|
|
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) ->
|
|
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(
|
|
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)
|
|
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)
|
|
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
|
-
) ->
|
|
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
|
-
) ->
|
|
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
|
|
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(
|
|
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:
|